Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hathor/feature_activation/bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def _log_signal_bits(self, feature: Feature, enable_bit: bool, support: bool, no

def _get_signaling_features(self, block: Block) -> dict[Feature, Criteria]:
"""Given a specific block, return all features that are in a signaling state for that block."""
feature_infos = self._feature_service.get_feature_infos(block=block)
feature_infos = self._feature_service.get_feature_infos(vertex=block)
signaling_features = {
feature: feature_info.criteria
for feature, feature_info in feature_infos.items()
Expand Down
34 changes: 27 additions & 7 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, TypeAlias

Expand All @@ -22,7 +24,7 @@

if TYPE_CHECKING:
from hathor.feature_activation.bit_signaling_service import BitSignalingService
from hathor.transaction import Block
from hathor.transaction import Block, Vertex
from hathor.transaction.storage import TransactionStorage


Expand All @@ -49,11 +51,20 @@ def __init__(self, *, settings: HathorSettings, tx_storage: 'TransactionStorage'
self._tx_storage = tx_storage
self.bit_signaling_service: Optional['BitSignalingService'] = None

def is_feature_active(self, *, block: 'Block', feature: Feature) -> bool:
"""Returns whether a Feature is active at a certain block."""
def is_feature_active(self, *, vertex: Vertex, feature: Feature) -> bool:
"""Return whether a Feature is active for a certain vertex."""
block = self._get_feature_activation_block(vertex)
state = self.get_state(block=block, feature=feature)
return state.is_active()

return state == FeatureState.ACTIVE
def _get_feature_activation_block(self, vertex: Vertex) -> Block:
"""Return the block used for feature activation depending on the vertex type."""
from hathor.transaction import Block, Transaction
if isinstance(vertex, Block):
return vertex
if isinstance(vertex, Transaction):
return self._tx_storage.get_block(vertex.static_metadata.closest_ancestor_block)
raise NotImplementedError

def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState:
"""
Expand All @@ -64,7 +75,7 @@ def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState
height = block.static_metadata.height
offset_to_boundary = height % self._feature_settings.evaluation_interval
remaining_blocks = self._feature_settings.evaluation_interval - offset_to_boundary - 1
feature_infos = self.get_feature_infos(block=block)
feature_infos = self.get_feature_infos(vertex=block)

must_signal_features = (
feature for feature, feature_info in feature_infos.items()
Expand Down Expand Up @@ -194,8 +205,9 @@ def _calculate_new_state(

raise NotImplementedError(f'Unknown previous state: {previous_state}')

def get_feature_infos(self, *, block: 'Block') -> dict[Feature, FeatureInfo]:
"""Returns the criteria definition and feature state for all features at a certain block."""
def get_feature_infos(self, *, vertex: Vertex) -> dict[Feature, FeatureInfo]:
"""Return the criteria definition and feature state for all features for a certain vertex."""
block = self._get_feature_activation_block(vertex)
return {
feature: FeatureInfo(
criteria=criteria,
Expand All @@ -204,6 +216,14 @@ def get_feature_infos(self, *, block: 'Block') -> dict[Feature, FeatureInfo]:
for feature, criteria in self._feature_settings.features.items()
}

def get_feature_states(self, *, vertex: Vertex) -> dict[Feature, FeatureState]:
"""Return the feature state for all features for a certain vertex."""
feature_infos = self.get_feature_infos(vertex=vertex)
return {
feature: info.state
for feature, info in feature_infos.items()
}

def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'Block':
"""
Given a block, return its ancestor at a specific height.
Expand Down
4 changes: 2 additions & 2 deletions hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def get_block_features(self, request: Request) -> bytes:
return error.json_dumpb()

signal_bits = []
feature_infos = self._feature_service.get_feature_infos(block=block)
feature_infos = self._feature_service.get_feature_infos(vertex=block)

for feature, feature_info in feature_infos.items():
if feature_info.state not in FeatureState.get_signaling_states():
Expand All @@ -90,7 +90,7 @@ def get_block_features(self, request: Request) -> bytes:
def get_features(self) -> bytes:
best_block = self.tx_storage.get_best_block()
bit_counts = best_block.static_metadata.feature_activation_bit_counts
feature_infos = self._feature_service.get_feature_infos(block=best_block)
feature_infos = self._feature_service.get_feature_infos(vertex=best_block)
features = []

for feature, feature_info in feature_infos.items():
Expand Down
61 changes: 54 additions & 7 deletions hathor/transaction/static_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from operator import add
from typing import TYPE_CHECKING, Callable

from typing_extensions import Self
from typing_extensions import Self, override

from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_state import FeatureState
Expand Down Expand Up @@ -57,6 +57,7 @@ def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticM
return BlockStaticMetadata(**json_dict)

if isinstance(target, Transaction):
json_dict['closest_ancestor_block'] = bytes.fromhex(json_dict['closest_ancestor_block'])
return TransactionStaticMetadata(**json_dict)

raise NotImplementedError
Expand Down Expand Up @@ -175,6 +176,10 @@ def _get_previous_feature_activation_bit_counts(


class TransactionStaticMetadata(VertexStaticMetadata):
# The Block with the greatest height that is a direct or indirect dependency (ancestor) of the transaction,
# including both funds and verification DAGs. It's used by Feature Activation for Transactions.
closest_ancestor_block: VertexId

@classmethod
def create_from_storage(cls, tx: 'Transaction', settings: HathorSettings, storage: 'TransactionStorage') -> Self:
"""Create a `TransactionStaticMetadata` using dependencies provided by a storage."""
Expand All @@ -189,14 +194,12 @@ def create(
) -> Self:
"""Create a `TransactionStaticMetadata` using dependencies provided by a `vertex_getter`.
This must be fast, ideally O(1)."""
min_height = cls._calculate_min_height(
tx,
settings,
vertex_getter=vertex_getter,
)
min_height = cls._calculate_min_height(tx, settings, vertex_getter)
closest_ancestor_block = cls._calculate_closest_ancestor_block(tx, settings, vertex_getter)

return cls(
min_height=min_height
min_height=min_height,
closest_ancestor_block=closest_ancestor_block,
)

@classmethod
Expand Down Expand Up @@ -245,3 +248,47 @@ def _calculate_my_min_height(
if isinstance(spent_tx, Block):
min_height = max(min_height, spent_tx.static_metadata.height + settings.REWARD_SPEND_MIN_BLOCKS + 1)
return min_height

@staticmethod
def _calculate_closest_ancestor_block(
tx: 'Transaction',
settings: HathorSettings,
vertex_getter: Callable[[VertexId], 'BaseTransaction'],
) -> VertexId:
"""
Calculate the tx's closest_ancestor_block. It's calculated by propagating the metadata forward in the DAG.
"""
from hathor.transaction import Block, Transaction
if tx.is_genesis:
return settings.GENESIS_BLOCK_HASH

closest_ancestor_block: Block | None = None

for vertex_id in tx.get_all_dependencies():
vertex = vertex_getter(vertex_id)
candidate_block: Block

if isinstance(vertex, Block):
candidate_block = vertex
elif isinstance(vertex, Transaction):
vertex_candidate = vertex_getter(vertex.static_metadata.closest_ancestor_block)
assert isinstance(vertex_candidate, Block)
candidate_block = vertex_candidate
else:
raise NotImplementedError

if (
not closest_ancestor_block
or candidate_block.static_metadata.height > closest_ancestor_block.static_metadata.height
):
closest_ancestor_block = candidate_block

assert closest_ancestor_block is not None
return closest_ancestor_block.hash

@override
def json_dumpb(self) -> bytes:
from hathor.util import json_dumpb
json_dict = self.dict()
json_dict['closest_ancestor_block'] = json_dict['closest_ancestor_block'].hex()
return json_dumpb(json_dict)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2023 Hathor Labs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING

from structlog import get_logger

from hathor.transaction.storage.migrations import BaseMigration

if TYPE_CHECKING:
from hathor.transaction.storage import TransactionStorage

logger = get_logger()


class Migration(BaseMigration):
def skip_empty_db(self) -> bool:
return True

def get_db_name(self) -> str:
return 'add_closest_ancestor_block'

def run(self, storage: 'TransactionStorage') -> None:
raise Exception('Cannot migrate your database due to an incompatible change in the metadata. '
'Please, delete your data folder and use the latest available snapshot or sync '
'from beginning.')
2 changes: 1 addition & 1 deletion hathor/verification/merge_mined_block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def verify_aux_pow(self, block: MergeMinedBlock) -> None:
assert block.aux_pow is not None

is_feature_active = self._feature_service.is_feature_active(
block=block,
vertex=block,
feature=Feature.INCREASE_MAX_MERKLE_PATH_LENGTH
)
max_merkle_path_length = (
Expand Down
16 changes: 7 additions & 9 deletions hathor/vertex_handler/vertex_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,24 +208,22 @@ def _log_new_object(self, tx: BaseTransaction, message_fmt: str, *, quiet: bool)
"""
metadata = tx.get_metadata()
now = datetime.datetime.fromtimestamp(self._reactor.seconds())
feature_states = self._feature_service.get_feature_states(vertex=tx)
kwargs = {
'tx': tx,
'ts_date': datetime.datetime.fromtimestamp(tx.timestamp),
'time_from_now': tx.get_time_from_now(now),
'validation': metadata.validation.name,
'feature_states': {
feature.value: state.value
for feature, state in feature_states.items()
}
}
if self._log_vertex_bytes:
kwargs['bytes'] = bytes(tx).hex()
if tx.is_block:
if isinstance(tx, Block):
message = message_fmt.format('block')
if isinstance(tx, Block):
feature_infos = self._feature_service.get_feature_infos(block=tx)
feature_states = {
feature.value: info.state.value
for feature, info in feature_infos.items()
}
kwargs['_height'] = tx.get_height()
kwargs['feature_states'] = feature_states
kwargs['_height'] = tx.get_height()
else:
message = message_fmt.format('tx')
if not quiet:
Expand Down
8 changes: 4 additions & 4 deletions tests/feature_activation/test_bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from hathor.feature_activation.model.feature_info import FeatureInfo
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.feature_activation.settings import Settings as FeatureSettings
from hathor.transaction import Block
from hathor.transaction import Block, Vertex
from hathor.transaction.storage import TransactionStorage


Expand Down Expand Up @@ -169,7 +169,7 @@ def _test_generate_signal_bits(
settings = Mock(spec_set=HathorSettings)
settings.FEATURE_ACTIVATION = FeatureSettings()
feature_service = Mock(spec_set=FeatureService)
feature_service.get_feature_infos = lambda block: feature_infos
feature_service.get_feature_infos = lambda vertex: feature_infos

service = BitSignalingService(
settings=settings,
Expand Down Expand Up @@ -264,8 +264,8 @@ def test_non_signaling_features_warning(
tx_storage = Mock(spec_set=TransactionStorage)
tx_storage.get_best_block = lambda: best_block

def get_feature_infos_mock(block: Block) -> dict[Feature, FeatureInfo]:
if block == best_block:
def get_feature_infos_mock(vertex: Vertex) -> dict[Feature, FeatureInfo]:
if vertex == best_block:
return {}
raise NotImplementedError

Expand Down
4 changes: 2 additions & 2 deletions tests/feature_activation/test_feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def test_is_feature_active(block_height: int) -> None:
service.bit_signaling_service = Mock()
block = not_none(storage.get_block_by_height(block_height))

result = service.is_feature_active(block=block, feature=Feature.NOP_FEATURE_1)
result = service.is_feature_active(vertex=block, feature=Feature.NOP_FEATURE_1)

assert result is True

Expand Down Expand Up @@ -505,7 +505,7 @@ def get_state(self: FeatureService, *, block: Block, feature: Feature) -> Featur
return states[feature]

with patch('hathor.feature_activation.feature_service.FeatureService.get_state', get_state):
result = service.get_feature_infos(block=Mock())
result = service.get_feature_infos(vertex=Mock(spec_set=Block))

expected = {
Feature.NOP_FEATURE_1: FeatureInfo(criteria_mock_1, FeatureState.STARTED),
Expand Down
Loading