diff --git a/hathor/builder/builder.py b/hathor/builder/builder.py index b4977820d..688738f44 100644 --- a/hathor/builder/builder.py +++ b/hathor/builder/builder.py @@ -29,7 +29,6 @@ from hathor.execution_manager import ExecutionManager from hathor.feature_activation.bit_signaling_service import BitSignalingService from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.storage.feature_activation_storage import FeatureActivationStorage from hathor.indexes import IndexesManager, MemoryIndexesManager, RocksDBIndexesManager from hathor.manager import HathorManager @@ -105,7 +104,6 @@ class BuildArtifacts(NamedTuple): pubsub: PubSubManager consensus: ConsensusAlgorithm tx_storage: TransactionStorage - feature_service: FeatureService bit_signaling_service: BitSignalingService indexes: Optional[IndexesManager] wallet: Optional[BaseWallet] @@ -113,10 +111,7 @@ class BuildArtifacts(NamedTuple): stratum_factory: Optional[StratumFactory] -_VertexVerifiersBuilder: TypeAlias = Callable[ - [HathorSettingsType, DifficultyAdjustmentAlgorithm, FeatureService], - VertexVerifiers -] +_VertexVerifiersBuilder: TypeAlias = Callable[[HathorSettingsType, DifficultyAdjustmentAlgorithm], VertexVerifiers] class Builder: @@ -149,7 +144,6 @@ def __init__(self) -> None: self._support_features: set[Feature] = set() self._not_support_features: set[Feature] = set() - self._feature_service: Optional[FeatureService] = None self._bit_signaling_service: Optional[BitSignalingService] = None self._daa: Optional[DifficultyAdjustmentAlgorithm] = None @@ -223,7 +217,6 @@ def build(self) -> BuildArtifacts: event_manager = self._get_or_create_event_manager() indexes = self._get_or_create_indexes_manager() tx_storage = self._get_or_create_tx_storage() - feature_service = self._get_or_create_feature_service() bit_signaling_service = self._get_or_create_bit_signaling_service() verification_service = self._get_or_create_verification_service() daa = self._get_or_create_daa() @@ -297,7 +290,6 @@ def build(self) -> BuildArtifacts: wallet=wallet, rocksdb_storage=self._rocksdb_storage, stratum_factory=stratum_factory, - feature_service=feature_service, bit_signaling_service=bit_signaling_service ) @@ -312,11 +304,6 @@ def set_event_manager(self, event_manager: EventManager) -> 'Builder': self._event_manager = event_manager return self - def set_feature_service(self, feature_service: FeatureService) -> 'Builder': - self.check_if_can_modify() - self._feature_service = feature_service - return self - def set_bit_signaling_service(self, bit_signaling_service: BitSignalingService) -> 'Builder': self.check_if_can_modify() self._bit_signaling_service = bit_signaling_service @@ -530,28 +517,19 @@ def _get_or_create_event_manager(self) -> EventManager: return self._event_manager - def _get_or_create_feature_service(self) -> FeatureService: - """Return the FeatureService instance set on this builder, or a new one if not set.""" - if self._feature_service is None: - settings = self._get_or_create_settings() - tx_storage = self._get_or_create_tx_storage() - self._feature_service = FeatureService(settings=settings, tx_storage=tx_storage) - - return self._feature_service - def _get_or_create_bit_signaling_service(self) -> BitSignalingService: if self._bit_signaling_service is None: settings = self._get_or_create_settings() tx_storage = self._get_or_create_tx_storage() - feature_service = self._get_or_create_feature_service() feature_storage = self._get_or_create_feature_storage() + pubsub = self._get_or_create_pubsub() self._bit_signaling_service = BitSignalingService( settings=settings, - feature_service=feature_service, tx_storage=tx_storage, support_features=self._support_features, not_support_features=self._not_support_features, feature_storage=feature_storage, + pubsub=pubsub, ) return self._bit_signaling_service @@ -581,17 +559,12 @@ def _get_or_create_feature_storage(self) -> FeatureActivationStorage | None: def _get_or_create_vertex_verifiers(self) -> VertexVerifiers: if self._vertex_verifiers is None: settings = self._get_or_create_settings() - feature_service = self._get_or_create_feature_service() daa = self._get_or_create_daa() if self._vertex_verifiers_builder: - self._vertex_verifiers = self._vertex_verifiers_builder(settings, daa, feature_service) + self._vertex_verifiers = self._vertex_verifiers_builder(settings, daa) else: - self._vertex_verifiers = VertexVerifiers.create_defaults( - settings=settings, - daa=daa, - feature_service=feature_service, - ) + self._vertex_verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa) return self._vertex_verifiers @@ -617,7 +590,6 @@ def _get_or_create_vertex_handler(self) -> VertexHandler: verification_service=self._get_or_create_verification_service(), consensus=self._get_or_create_consensus(), p2p_manager=self._get_or_create_p2p_manager(), - feature_service=self._get_or_create_feature_service(), pubsub=self._get_or_create_pubsub(), wallet=self._get_or_create_wallet(), ) diff --git a/hathor/builder/cli_builder.py b/hathor/builder/cli_builder.py index 4548c5077..e77fc0ee1 100644 --- a/hathor/builder/cli_builder.py +++ b/hathor/builder/cli_builder.py @@ -29,7 +29,6 @@ from hathor.exception import BuilderError from hathor.execution_manager import ExecutionManager from hathor.feature_activation.bit_signaling_service import BitSignalingService -from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.storage.feature_activation_storage import FeatureActivationStorage from hathor.indexes import IndexesManager, MemoryIndexesManager, RocksDBIndexesManager from hathor.manager import HathorManager @@ -283,15 +282,13 @@ def create_manager(self, reactor: Reactor) -> HathorManager: self.log.info('--x-enable-event-queue flag provided. ' 'The events detected by the full node will be stored and can be retrieved by clients') - self.feature_service = FeatureService(settings=settings, tx_storage=tx_storage) - bit_signaling_service = BitSignalingService( settings=settings, - feature_service=self.feature_service, tx_storage=tx_storage, support_features=self._args.signal_support, not_support_features=self._args.signal_not_support, feature_storage=feature_storage, + pubsub=pubsub, ) test_mode = TestMode.DISABLED @@ -302,11 +299,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager: daa = DifficultyAdjustmentAlgorithm(settings=settings, test_mode=test_mode) - vertex_verifiers = VertexVerifiers.create_defaults( - settings=settings, - daa=daa, - feature_service=self.feature_service - ) + vertex_verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa) verification_service = VerificationService( settings=settings, verifiers=vertex_verifiers, @@ -334,7 +327,6 @@ def create_manager(self, reactor: Reactor) -> HathorManager: verification_service=verification_service, consensus=consensus_algorithm, p2p_manager=p2p_manager, - feature_service=self.feature_service, pubsub=pubsub, wallet=self.wallet, log_vertex_bytes=self._args.log_vertex_bytes, diff --git a/hathor/builder/resources_builder.py b/hathor/builder/resources_builder.py index ce453aef6..e7c9de909 100644 --- a/hathor/builder/resources_builder.py +++ b/hathor/builder/resources_builder.py @@ -23,7 +23,6 @@ from hathor.event.resources.event import EventResource from hathor.exception import BuilderError -from hathor.feature_activation.feature_service import FeatureService from hathor.prometheus import PrometheusMetricsExporter if TYPE_CHECKING: @@ -65,7 +64,6 @@ def __init__( manager: 'HathorManager', args: 'RunNodeArgs', event_ws_factory: Optional['EventWebsocketFactory'], - feature_service: FeatureService ) -> None: self.log = logger.new() self.manager = manager @@ -76,8 +74,6 @@ def __init__( self._built_status = False self._built_prometheus = False - self._feature_service = feature_service - def build(self) -> Optional[server.Site]: if self._args.prometheus: self.create_prometheus() @@ -236,11 +232,7 @@ def create_resources(self) -> server.Site: # Feature Activation ( b'feature', - FeatureResource( - settings=settings, - feature_service=self._feature_service, - tx_storage=self.manager.tx_storage - ), + FeatureResource(settings=settings, tx_storage=self.manager.tx_storage), root ) ] diff --git a/hathor/cli/mining.py b/hathor/cli/mining.py index bb8655a82..7e386194a 100644 --- a/hathor/cli/mining.py +++ b/hathor/cli/mining.py @@ -133,15 +133,13 @@ def execute(args: Namespace) -> None: block.nonce, block.weight)) try: - from unittest.mock import Mock - from hathor.conf.get_settings import get_global_settings from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.verification.verification_service import VerificationService from hathor.verification.vertex_verifiers import VertexVerifiers settings = get_global_settings() daa = DifficultyAdjustmentAlgorithm(settings=settings) - verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa, feature_service=Mock()) + verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa) verification_service = VerificationService(settings=settings, verifiers=verifiers) verification_service.verify_without_storage(block) except HathorError: diff --git a/hathor/cli/run_node.py b/hathor/cli/run_node.py index 88489fa8a..297ae6dd1 100644 --- a/hathor/cli/run_node.py +++ b/hathor/cli/run_node.py @@ -212,7 +212,6 @@ def prepare(self, *, register_resources: bool = True) -> None: self.manager, self._args, builder.event_ws_factory, - builder.feature_service ) status_server = resources_builder.build() if self._args.status: @@ -234,7 +233,6 @@ def prepare(self, *, register_resources: bool = True) -> None: wallet=self.manager.wallet, rocksdb_storage=getattr(builder, 'rocksdb_storage', None), stratum_factory=self.manager.stratum_factory, - feature_service=self.manager.vertex_handler._feature_service, bit_signaling_service=self.manager._bit_signaling_service, ) diff --git a/hathor/feature_activation/bit_signaling_service.py b/hathor/feature_activation/bit_signaling_service.py index 3d21c32e5..e82a1d42a 100644 --- a/hathor/feature_activation/bit_signaling_service.py +++ b/hathor/feature_activation/bit_signaling_service.py @@ -16,10 +16,10 @@ from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.model.criteria import Criteria from hathor.feature_activation.model.feature_state import FeatureState from hathor.feature_activation.storage.feature_activation_storage import FeatureActivationStorage +from hathor.pubsub import EventArguments, HathorEvents, PubSubManager from hathor.transaction import Block from hathor.transaction.storage import TransactionStorage @@ -30,8 +30,8 @@ class BitSignalingService: __slots__ = ( '_log', '_settings', - '_feature_service', '_tx_storage', + '_pubsub', '_support_features', '_not_support_features', '_feature_storage', @@ -41,22 +41,21 @@ def __init__( self, *, settings: HathorSettings, - feature_service: FeatureService, tx_storage: TransactionStorage, + pubsub: PubSubManager, support_features: set[Feature], not_support_features: set[Feature], feature_storage: FeatureActivationStorage | None, ) -> None: self._log = logger.new() self._settings = settings - self._feature_service = feature_service self._tx_storage = tx_storage + self._pubsub = pubsub self._support_features = support_features self._not_support_features = not_support_features self._feature_storage = feature_storage self._validate_support_intersection() - self._feature_service.bit_signaling_service = self def start(self) -> None: """ @@ -70,6 +69,7 @@ def start(self) -> None: self._warn_non_signaling_features(best_block) self._log_feature_signals(best_block) + self._pubsub.subscribe(HathorEvents.NETWORK_NEW_TX_ACCEPTED, self._on_new_vertex) def generate_signal_bits(self, *, block: Block, log: bool = False) -> int: """ @@ -137,11 +137,27 @@ def remove_feature_support(self, feature: Feature) -> None: self._support_features.discard(feature) self._not_support_features.add(feature) - def on_must_signal(self, feature: Feature) -> None: + def _on_new_vertex(self, hathor_event: HathorEvents, event_args: EventArguments) -> None: """ - When the MUST_SIGNAL phase is reached, feature support is automatically enabled. + When a new block is received, if it's the first block in the `MUST_SIGNAL` phase for a feature, + then feature support is automatically enabled for that feature. """ - self.add_feature_support(feature) + assert hathor_event is HathorEvents.NETWORK_NEW_TX_ACCEPTED + vertex = event_args.tx + if not isinstance(vertex, Block) or vertex.is_genesis: + return + + parent_block = vertex.get_block_parent() + block_feature_infos = vertex.static_metadata.get_feature_infos(self._settings) + + for feature, block_feature_info in block_feature_infos.items(): + parent_feature_state = parent_block.static_metadata.get_feature_state(feature) + + if ( + block_feature_info.state is FeatureState.MUST_SIGNAL + and parent_feature_state is not FeatureState.MUST_SIGNAL + ): + self.add_feature_support(feature) def _log_signal_bits(self, feature: Feature, enable_bit: bool, support: bool, not_support: bool) -> None: """Generate info log for a feature's signal.""" @@ -163,7 +179,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 = block.static_metadata.get_feature_infos(self._settings) signaling_features = { feature: feature_info.criteria for feature, feature_info in feature_infos.items() diff --git a/hathor/feature_activation/feature_service.py b/hathor/feature_activation/feature_service.py index 2b8212cef..529509cd0 100644 --- a/hathor/feature_activation/feature_service.py +++ b/hathor/feature_activation/feature_service.py @@ -13,17 +13,15 @@ # limitations under the License. from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional, TypeAlias +from typing import TYPE_CHECKING, Callable, Optional, TypeAlias from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature -from hathor.feature_activation.model.feature_info import FeatureInfo from hathor.feature_activation.model.feature_state import FeatureState +from hathor.types import VertexId if TYPE_CHECKING: - from hathor.feature_activation.bit_signaling_service import BitSignalingService - from hathor.transaction import Block - from hathor.transaction.storage import TransactionStorage + from hathor.transaction import Block, Vertex @dataclass(frozen=True, slots=True) @@ -42,29 +40,31 @@ class BlockIsMissingSignal: class FeatureService: - __slots__ = ('_feature_settings', '_tx_storage', 'bit_signaling_service') + __slots__ = ('_feature_settings', '_vertex_getter', '_block_by_height_getter') - def __init__(self, *, settings: HathorSettings, tx_storage: 'TransactionStorage') -> None: + def __init__( + self, + *, + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'Vertex'], + block_by_height_getter: Callable[[int], Optional['Block']], + ) -> None: self._feature_settings = settings.FEATURE_ACTIVATION - 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.""" - state = self.get_state(block=block, feature=feature) - - return state == FeatureState.ACTIVE + self._vertex_getter = vertex_getter + self._block_by_height_getter = block_by_height_getter - def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState: + @staticmethod + def is_signaling_mandatory_features(block: 'Block', settings: HathorSettings) -> BlockSignalingState: """ Return whether a block is signaling features that are mandatory, that is, any feature currently in the MUST_SIGNAL phase. """ + feature_settings = settings.FEATURE_ACTIVATION bit_counts = block.static_metadata.feature_activation_bit_counts 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) + offset_to_boundary = height % feature_settings.evaluation_interval + remaining_blocks = feature_settings.evaluation_interval - offset_to_boundary - 1 + feature_infos = block.static_metadata.get_feature_infos(settings) must_signal_features = ( feature for feature, feature_info in feature_infos.items() @@ -72,8 +72,8 @@ def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState ) for feature in must_signal_features: - criteria = self._feature_settings.features[feature] - threshold = criteria.get_threshold(self._feature_settings) + criteria = feature_settings.features[feature] + threshold = criteria.get_threshold(feature_settings) count = bit_counts[criteria.bit] missing_signals = threshold - count @@ -82,53 +82,45 @@ def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState return BlockIsSignaling() - def get_state(self, *, block: 'Block', feature: Feature) -> FeatureState: - """Returns the state of a feature at a certain block. Uses block metadata to cache states.""" + def calculate_all_feature_states(self, block: 'Block', *, height: int) -> dict[Feature, FeatureState]: + """Calculate the state of all features at a certain block.""" + return { + feature: self._calculate_state(block=block, height=height, feature=feature) + for feature in self._feature_settings.features + } + + def _calculate_state(self, *, block: 'Block', height: int, feature: Feature) -> FeatureState: + """Calculate the state of a feature at a certain block.""" # per definition, the genesis block is in the DEFINED state for all features if block.is_genesis: return FeatureState.DEFINED - if state := block.get_feature_state(feature=feature): - return state + from hathor.transaction import Block + parent_hash = block.get_block_parent_hash() + parent_block = self._vertex_getter(parent_hash) + assert isinstance(parent_block, Block) + previous_state = parent_block.static_metadata.get_feature_state(feature) # All blocks within the same evaluation interval have the same state, that is, the state is only defined for - # the block in each interval boundary. Therefore, we get the state of the previous boundary block or calculate + # the block in each interval boundary. Therefore, we get the state of the previous block or calculate # a new state if this block is a boundary block. - height = block.static_metadata.height - offset_to_boundary = height % self._feature_settings.evaluation_interval - offset_to_previous_boundary = offset_to_boundary or self._feature_settings.evaluation_interval - previous_boundary_height = height - offset_to_previous_boundary - assert previous_boundary_height >= 0 - previous_boundary_block = self._get_ancestor_at_height(block=block, ancestor_height=previous_boundary_height) - previous_boundary_state = self.get_state(block=previous_boundary_block, feature=feature) - - # We cache _and save_ the state of the previous boundary block that we just got. - previous_boundary_block.set_feature_state(feature=feature, state=previous_boundary_state, save=True) + is_boundary_block = height % self._feature_settings.evaluation_interval == 0 + if not is_boundary_block: + return previous_state - if offset_to_boundary != 0: - return previous_boundary_state - - new_state = self._calculate_new_state( + return self._calculate_new_state( boundary_block=block, + height=height, feature=feature, - previous_state=previous_boundary_state + previous_state=previous_state ) - if new_state == FeatureState.MUST_SIGNAL: - assert self.bit_signaling_service is not None - self.bit_signaling_service.on_must_signal(feature) - - # We cache the just calculated state of the current block _without saving it_, as it may still be unverified, - # so we cannot persist its metadata. That's why we cache and save the previous boundary block above. - block.set_feature_state(feature=feature, state=new_state) - - return new_state - def _calculate_new_state( self, *, boundary_block: 'Block', + height: int, feature: Feature, previous_state: FeatureState ) -> FeatureState: @@ -139,7 +131,6 @@ def _calculate_new_state( an AssertionError. Non-boundary blocks never calculate their own state, they get it from their parent block instead. """ - height = boundary_block.static_metadata.height criteria = self._feature_settings.features.get(feature) evaluation_interval = self._feature_settings.evaluation_interval @@ -148,6 +139,7 @@ def _calculate_new_state( assert not boundary_block.is_genesis, 'cannot calculate new state for genesis' assert height % evaluation_interval == 0, 'cannot calculate new state for a non-boundary block' + from hathor.transaction import Block if previous_state is FeatureState.DEFINED: if height >= criteria.start_height: @@ -161,7 +153,9 @@ def _calculate_new_state( # Get the count for this block's parent. Since this is a boundary block, its parent count represents the # previous evaluation interval count. - parent_block = boundary_block.get_block_parent() + parent_block_hash = boundary_block.get_block_parent_hash() + parent_block = self._vertex_getter(parent_block_hash) + assert isinstance(parent_block, Block) counts = parent_block.static_metadata.feature_activation_bit_counts count = counts[criteria.bit] threshold = criteria.get_threshold(self._feature_settings) @@ -193,58 +187,3 @@ def _calculate_new_state( return FeatureState.FAILED 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.""" - return { - feature: FeatureInfo( - criteria=criteria, - state=self.get_state(block=block, feature=feature) - ) - for feature, criteria in self._feature_settings.features.items() - } - - def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'Block': - """ - Given a block, return its ancestor at a specific height. - Uses the height index if the block is in the best blockchain, or search iteratively otherwise. - """ - assert ancestor_height < block.static_metadata.height, ( - f"ancestor height must be lower than the block's height: " - f"{ancestor_height} >= {block.static_metadata.height}" - ) - - # It's possible that this method is called before the consensus runs for this block, therefore we do not know - # if it's in the best blockchain. For this reason, we have to get the ancestor starting from our parent block. - parent_block = block.get_block_parent() - parent_metadata = parent_block.get_metadata() - assert parent_metadata.validation.is_fully_connected(), 'The parent should always be fully validated.' - - if parent_block.static_metadata.height == ancestor_height: - return parent_block - - if not parent_metadata.voided_by: - ancestor = self._tx_storage.get_block_by_height(ancestor_height) - assert ancestor is not None, ( - 'it is guaranteed that the ancestor of a fully connected and non-voided block is in the height index' - ) - return ancestor - - return self._get_ancestor_iteratively(block=parent_block, ancestor_height=ancestor_height) - - def _get_ancestor_iteratively(self, *, block: 'Block', ancestor_height: int) -> 'Block': - """ - Given a block, return its ancestor at a specific height by iterating over its ancestors. - This is slower than using the height index. - """ - # TODO: there are further optimizations to be done here, the latest common block height could be persisted in - # metadata, so we could still use the height index if the requested height is before that height. - assert ancestor_height >= 0 - assert block.static_metadata.height - ancestor_height <= self._feature_settings.evaluation_interval, ( - 'requested ancestor is deeper than the maximum allowed' - ) - ancestor = block - while ancestor.static_metadata.height > ancestor_height: - ancestor = ancestor.get_block_parent() - - return ancestor diff --git a/hathor/feature_activation/resources/feature.py b/hathor/feature_activation/resources/feature.py index 75e7c16bf..80e20e3d6 100644 --- a/hathor/feature_activation/resources/feature.py +++ b/hathor/feature_activation/resources/feature.py @@ -20,7 +20,6 @@ from hathor.cli.openapi_files.register import register_resource from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.model.feature_state import FeatureState from hathor.transaction import Block from hathor.transaction.storage import TransactionStorage @@ -33,16 +32,10 @@ class FeatureResource(Resource): isLeaf = True - def __init__( - self, - *, - settings: HathorSettings, - feature_service: FeatureService, - tx_storage: TransactionStorage - ) -> None: + def __init__(self, *, settings: HathorSettings, tx_storage: TransactionStorage) -> None: super().__init__() + self._settings = settings self._feature_settings = settings.FEATURE_ACTIVATION - self._feature_service = feature_service self.tx_storage = tx_storage def render_GET(self, request: Request) -> bytes: @@ -68,7 +61,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 = block.static_metadata.get_feature_infos(self._settings) for feature, feature_info in feature_infos.items(): if feature_info.state not in FeatureState.get_signaling_states(): @@ -90,7 +83,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 = best_block.static_metadata.get_feature_infos(self._settings) features = [] for feature, feature_info in feature_infos.items(): diff --git a/hathor/manager.py b/hathor/manager.py index 4f09d126c..aa2d7decc 100644 --- a/hathor/manager.py +++ b/hathor/manager.py @@ -527,6 +527,8 @@ def _initialize_components_full_verification(self) -> None: self.tx_storage.indexes._manually_initialize(self.tx_storage) + self._bit_signaling_service.start() + self.log.debug('done loading transactions') # Check if all checkpoints in database are ok diff --git a/hathor/simulator/simulator.py b/hathor/simulator/simulator.py index c1d0754f4..9ead9bd03 100644 --- a/hathor/simulator/simulator.py +++ b/hathor/simulator/simulator.py @@ -24,7 +24,6 @@ from hathor.conf.get_settings import get_global_settings from hathor.conf.settings import HathorSettings from hathor.daa import DifficultyAdjustmentAlgorithm -from hathor.feature_activation.feature_service import FeatureService from hathor.manager import HathorManager from hathor.p2p.peer import Peer from hathor.simulator.clock import HeapClock, MemoryReactorHeapClock @@ -243,11 +242,7 @@ def run(self, return True -def _build_vertex_verifiers( - settings: HathorSettings, - daa: DifficultyAdjustmentAlgorithm, - feature_service: FeatureService -) -> VertexVerifiers: +def _build_vertex_verifiers(settings: HathorSettings, daa: DifficultyAdjustmentAlgorithm) -> VertexVerifiers: """ A custom VertexVerifiers builder to be used by the simulator. """ @@ -255,5 +250,4 @@ def _build_vertex_verifiers( settings=settings, vertex_verifier=SimulatorVertexVerifier(settings=settings), daa=daa, - feature_service=feature_service, ) diff --git a/hathor/transaction/block.py b/hathor/transaction/block.py index 9bdeb6ac9..3e9090231 100644 --- a/hathor/transaction/block.py +++ b/hathor/transaction/block.py @@ -21,8 +21,6 @@ from typing_extensions import Self, override from hathor.checkpoint import Checkpoint -from hathor.feature_activation.feature import Feature -from hathor.feature_activation.model.feature_state import FeatureState from hathor.transaction import BaseTransaction, TxOutput, TxVersion from hathor.transaction.base_transaction import GenericVertex from hathor.transaction.exceptions import CheckpointError @@ -318,38 +316,6 @@ def _get_feature_activation_bitmask(self) -> int: return bitmask - def get_feature_state(self, *, feature: Feature) -> Optional[FeatureState]: - """Returns the state of a feature from metadata.""" - metadata = self.get_metadata() - feature_states = metadata.feature_states or {} - - return feature_states.get(feature) - - def set_feature_state(self, *, feature: Feature, state: FeatureState, save: bool = False) -> None: - """ - Set the state of a feature in metadata, if it's not set. Fails if it's set and the value is different. - - Args: - feature: the feature to set the state of. - state: the state to set. - save: whether to save this block's metadata in storage. - """ - previous_state = self.get_feature_state(feature=feature) - - if state == previous_state: - return - - assert previous_state is None - assert self.storage is not None - - metadata = self.get_metadata() - feature_states = metadata.feature_states or {} - feature_states[feature] = state - metadata.feature_states = feature_states - - if save: - self.storage.save_transaction(self, only_metadata=True) - def get_feature_activation_bit_value(self, bit: int) -> int: """Get the feature activation bit value for a specific bit position.""" bit_list = self._get_feature_activation_bit_list() diff --git a/hathor/transaction/static_metadata.py b/hathor/transaction/static_metadata.py index d52c435e8..590050c6d 100644 --- a/hathor/transaction/static_metadata.py +++ b/hathor/transaction/static_metadata.py @@ -17,11 +17,12 @@ from abc import ABC from itertools import chain, starmap, zip_longest from operator import add -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Optional from typing_extensions import Self from hathor.feature_activation.feature import Feature +from hathor.feature_activation.model.feature_info import FeatureInfo from hathor.feature_activation.model.feature_state import FeatureState from hathor.types import VertexId from hathor.util import json_loadb @@ -76,19 +77,27 @@ class BlockStaticMetadata(VertexStaticMetadata): @classmethod def create_from_storage(cls, block: 'Block', settings: HathorSettings, storage: 'TransactionStorage') -> Self: """Create a `BlockStaticMetadata` using dependencies provided by a storage.""" - return cls.create(block, settings, storage.get_vertex) + return cls.create(block, settings, storage.get_vertex, storage.get_block_by_height) @classmethod def create( cls, block: 'Block', settings: HathorSettings, - vertex_getter: Callable[[VertexId], 'BaseTransaction'] + vertex_getter: Callable[[VertexId], 'BaseTransaction'], + block_by_height_getter: Callable[[int], Optional['Block']], ) -> Self: """Create a `BlockStaticMetadata` using dependencies provided by a `vertex_getter`. This must be fast, ideally O(1).""" + from hathor.feature_activation.feature_service import FeatureService + feature_service = FeatureService( + settings=settings, + vertex_getter=vertex_getter, + block_by_height_getter=block_by_height_getter, + ) height = cls._calculate_height(block, vertex_getter) min_height = cls._calculate_min_height(block, vertex_getter) + feature_states = feature_service.calculate_all_feature_states(block, height=height) feature_activation_bit_counts = cls._calculate_feature_activation_bit_counts( block, height, @@ -100,7 +109,7 @@ def create( height=height, min_height=min_height, feature_activation_bit_counts=feature_activation_bit_counts, - feature_states={}, # This will be populated in the next PR + feature_states=feature_states, ) @staticmethod @@ -173,6 +182,24 @@ def _get_previous_feature_activation_bit_counts( return parent_block.static_metadata.feature_activation_bit_counts + def get_feature_state(self, feature: Feature) -> FeatureState: + """Return the feature state for this block.""" + return self.feature_states.get(feature, FeatureState.DEFINED) + + def is_feature_active(self, feature: Feature) -> bool: + """Return whether a feature is active for this block.""" + return self.get_feature_state(feature).is_active() + + def get_feature_infos(self, settings: HathorSettings) -> dict[Feature, FeatureInfo]: + """Return the criteria definition and feature state for all features.""" + return { + feature: FeatureInfo( + criteria=criteria, + state=self.get_feature_state(feature) + ) + for feature, criteria in settings.FEATURE_ACTIVATION.features.items() + } + class TransactionStaticMetadata(VertexStaticMetadata): @classmethod diff --git a/hathor/transaction/storage/migrations/migrate_feature_states.py b/hathor/transaction/storage/migrations/migrate_feature_states.py new file mode 100644 index 000000000..e521ef8f1 --- /dev/null +++ b/hathor/transaction/storage/migrations/migrate_feature_states.py @@ -0,0 +1,70 @@ +# 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.conf.get_settings import get_global_settings +from hathor.transaction import Block, Transaction +from hathor.transaction.static_metadata import BlockStaticMetadata, TransactionStaticMetadata +from hathor.transaction.storage.migrations import BaseMigration +from hathor.util import progress + +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 'migrate_feature_states' + + def run(self, storage: 'TransactionStorage') -> None: + """This migration calculates feature states for blocks and saves them as static metadata.""" + log = logger.new() + settings = get_global_settings() + topological_iter = storage.topological_iterator() + + # The static metadata is set by the previous migration, so we can use the topological iterator normally + for vertex in progress(topological_iter, log=log, total=None): + if isinstance(vertex, Block): + # We create the static metadata from scratch, which now includes feature states + new_static_metadata = BlockStaticMetadata.create_from_storage(vertex, settings, storage) + + # We validate that it's the same as the current static metadata, except for feature states + if vertex.is_genesis: + assert vertex.static_metadata == new_static_metadata + else: + assert vertex.static_metadata.feature_states == {} + assert vertex.static_metadata == new_static_metadata.copy(update=dict(feature_states={})) + + # We set the new static metadata manually + vertex._static_metadata = new_static_metadata + elif isinstance(vertex, Transaction): + # We re-create the static metadata from scratch and compare it with the value that was created by the + # previous migration, as a sanity check. + assert vertex.static_metadata == TransactionStaticMetadata.create_from_storage( + vertex, settings, storage + ) + else: + raise NotImplementedError + + # We re-save the vertex's metadata so it's serialized with the new `to_bytes()` method, excluding fields + # that were migrated. + storage.save_transaction(vertex, only_metadata=True) diff --git a/hathor/transaction/storage/migrations/migrate_static_metadata.py b/hathor/transaction/storage/migrations/migrate_static_metadata.py index 2edb3adf4..1b4ef54c0 100644 --- a/hathor/transaction/storage/migrations/migrate_static_metadata.py +++ b/hathor/transaction/storage/migrations/migrate_static_metadata.py @@ -16,11 +16,7 @@ from structlog import get_logger -from hathor.conf.get_settings import get_global_settings -from hathor.transaction import Block, Transaction -from hathor.transaction.static_metadata import BlockStaticMetadata, TransactionStaticMetadata from hathor.transaction.storage.migrations import BaseMigration -from hathor.util import progress if TYPE_CHECKING: from hathor.transaction.storage import TransactionStorage @@ -38,30 +34,6 @@ def get_db_name(self) -> str: def run(self, storage: 'TransactionStorage') -> None: """This migration takes attributes from existing vertex metadata and saves them as static metadata.""" log = logger.new() - settings = get_global_settings() - # First we migrate static metadata using the storage itself since it uses internal structures. - log.info('creating static metadata...') + # We migrate static metadata using the storage itself since it uses internal structures. storage.migrate_static_metadata(log) - - # Now that static metadata is set, we can use the topological iterator normally - log.info('removing old metadata and validating...') - topological_iter = storage.topological_iterator() - - for vertex in progress(topological_iter, log=log, total=None): - # We re-save the vertex's metadata so it's serialized with the new `to_bytes()` method, excluding fields - # that were migrated. - storage.save_transaction(vertex, only_metadata=True) - - # We re-create the static metadata from scratch and compare it with the value that was created by the - # migration above, as a sanity check. - if isinstance(vertex, Block): - assert vertex.static_metadata == BlockStaticMetadata.create_from_storage( - vertex, settings, storage - ) - elif isinstance(vertex, Transaction): - assert vertex.static_metadata == TransactionStaticMetadata.create_from_storage( - vertex, settings, storage - ) - else: - raise NotImplementedError diff --git a/hathor/transaction/storage/migrations/remove_first_nop_features.py b/hathor/transaction/storage/migrations/remove_first_nop_features.py index c5ede9997..49c16b577 100644 --- a/hathor/transaction/storage/migrations/remove_first_nop_features.py +++ b/hathor/transaction/storage/migrations/remove_first_nop_features.py @@ -16,10 +16,7 @@ from structlog import get_logger -from hathor.conf.get_settings import get_global_settings -from hathor.transaction import Block from hathor.transaction.storage.migrations import BaseMigration -from hathor.util import progress if TYPE_CHECKING: from hathor.transaction.storage import TransactionStorage @@ -38,21 +35,5 @@ def run(self, storage: 'TransactionStorage') -> None: """ This migration clears the Feature Activation metadata related to the first Phased Testing on testnet. """ - settings = get_global_settings() log = logger.new() - - if settings.NETWORK_NAME != 'testnet-golf': - # If it's not testnet, we don't have to clear anything. - log.info('Skipping testnet-only migration.') - return - - topological_iterator = storage.topological_iterator() - - for vertex in progress(topological_iterator, log=log, total=None): - if isinstance(vertex, Block): - meta = vertex.get_metadata() - # This is the start_height of the **second** Phased Testing, so we clear anything before it. - if vertex.static_metadata.height < 3_386_880: - meta.feature_states = None - - storage.save_transaction(vertex, only_metadata=True) + log.info('Skipping migration as it will run on migrate_feature_states.') diff --git a/hathor/transaction/storage/migrations/remove_second_nop_features.py b/hathor/transaction/storage/migrations/remove_second_nop_features.py index dd322b1f7..7f85098c0 100644 --- a/hathor/transaction/storage/migrations/remove_second_nop_features.py +++ b/hathor/transaction/storage/migrations/remove_second_nop_features.py @@ -16,9 +16,7 @@ from structlog import get_logger -from hathor.conf.get_settings import get_global_settings from hathor.transaction.storage.migrations import BaseMigration -from hathor.util import progress if TYPE_CHECKING: from hathor.transaction.storage import TransactionStorage @@ -37,18 +35,5 @@ def run(self, storage: 'TransactionStorage') -> None: """ This migration clears the Feature Activation metadata related to the second Phased Testing on testnet. """ - settings = get_global_settings() log = logger.new() - - if settings.NETWORK_NAME != 'testnet-golf': - # If it's not testnet, we don't have to clear anything. - log.info('Skipping testnet-only migration.') - return - - topological_iterator = storage.topological_iterator() - - for vertex in progress(topological_iterator, log=log, total=None): - if vertex.is_block: - meta = vertex.get_metadata() - meta.feature_states = None - storage.save_transaction(vertex, only_metadata=True) + log.info('Skipping migration as it will run on migrate_feature_states.') diff --git a/hathor/transaction/storage/rocksdb_storage.py b/hathor/transaction/storage/rocksdb_storage.py index efe8f607e..d937372f2 100644 --- a/hathor/transaction/storage/rocksdb_storage.py +++ b/hathor/transaction/storage/rocksdb_storage.py @@ -263,7 +263,7 @@ def migrate_static_metadata(self, log: BoundLogger) -> None: height=height, min_height=min_height, feature_activation_bit_counts=bit_counts, - feature_states={}, # This will be populated in the next PR + feature_states={}, # We leave it empty because migrate_feature_states will populate it ) else: assert bit_counts is None or bit_counts == [] diff --git a/hathor/transaction/storage/transaction_storage.py b/hathor/transaction/storage/transaction_storage.py index ec1492939..18f390d84 100644 --- a/hathor/transaction/storage/transaction_storage.py +++ b/hathor/transaction/storage/transaction_storage.py @@ -44,6 +44,7 @@ add_feature_activation_bit_counts_metadata, add_feature_activation_bit_counts_metadata2, add_min_height_metadata, + migrate_feature_states, migrate_static_metadata, remove_first_nop_features, remove_second_nop_features, @@ -103,6 +104,7 @@ class TransactionStorage(ABC): add_feature_activation_bit_counts_metadata2.Migration, remove_second_nop_features.Migration, migrate_static_metadata.Migration, + migrate_feature_states.Migration, ] _migrations: list[BaseMigration] diff --git a/hathor/transaction/transaction_metadata.py b/hathor/transaction/transaction_metadata.py index db4279e96..f2b6c5c5b 100644 --- a/hathor/transaction/transaction_metadata.py +++ b/hathor/transaction/transaction_metadata.py @@ -18,8 +18,6 @@ from typing import TYPE_CHECKING, Any, Optional from hathor.conf.get_settings import get_global_settings -from hathor.feature_activation.feature import Feature -from hathor.feature_activation.model.feature_state import FeatureState from hathor.transaction.validation_state import ValidationState from hathor.util import json_dumpb, json_loadb, practically_equal @@ -45,10 +43,6 @@ class TransactionMetadata: first_block: Optional[bytes] validation: ValidationState - # A dict of features in the feature activation process and their respective state. Must only be used by Blocks, - # is None otherwise. This is only used for caching, so it can be safely cleared up, as it would be recalculated - # when necessary. - feature_states: Optional[dict[Feature, FeatureState]] = None # It must be a weakref. _tx_ref: Optional['ReferenceType[BaseTransaction]'] @@ -174,8 +168,7 @@ def __eq__(self, other: Any) -> bool: if not isinstance(other, TransactionMetadata): return False for field in ['hash', 'conflict_with', 'voided_by', 'received_by', 'children', - 'accumulated_weight', 'twins', 'score', 'first_block', 'validation', - 'feature_states']: + 'accumulated_weight', 'twins', 'score', 'first_block', 'validation']: if (getattr(self, field) or None) != (getattr(other, field) or None): return False @@ -214,17 +207,16 @@ def to_json(self) -> dict[str, Any]: from hathor.transaction import Block if isinstance(vertex, Block): + feature_states = vertex.static_metadata.feature_states data['height'] = vertex.static_metadata.height data['feature_activation_bit_counts'] = vertex.static_metadata.feature_activation_bit_counts + data['feature_states'] = {feature.value: state.value for feature, state in feature_states.items()} else: # TODO: This is kept here backwards compatibility with transactions, # but should be removed in the future. data['height'] = 0 data['feature_activation_bit_counts'] = [] - if self.feature_states is not None: - data['feature_states'] = {feature.value: state.value for feature, state in self.feature_states.items()} - if self.first_block is not None: data['first_block'] = self.first_block.hex() else: @@ -271,13 +263,6 @@ def create_from_json(cls, data: dict[str, Any]) -> 'TransactionMetadata': meta.accumulated_weight = data['accumulated_weight'] meta.score = data.get('score', 0) - feature_states_raw = data.get('feature_states') - if feature_states_raw: - meta.feature_states = { - Feature(feature): FeatureState(feature_state) - for feature, feature_state in feature_states_raw.items() - } - first_block_raw = data.get('first_block', None) if first_block_raw: meta.first_block = bytes.fromhex(first_block_raw) @@ -304,9 +289,8 @@ def to_bytes(self) -> bytes: del json_dict['min_height'] if 'feature_activation_bit_counts' in json_dict: del json_dict['feature_activation_bit_counts'] - # TODO: This one has not been migrated yet, but will be in the next PR - # if 'feature_states' in json_dict: - # del json_dict['feature_states'] + if 'feature_states' in json_dict: + del json_dict['feature_states'] return json_dumpb(json_dict) diff --git a/hathor/verification/block_verifier.py b/hathor/verification/block_verifier.py index 21e50eedd..54ebc8f84 100644 --- a/hathor/verification/block_verifier.py +++ b/hathor/verification/block_verifier.py @@ -30,18 +30,11 @@ class BlockVerifier: - __slots__ = ('_settings', '_daa', '_feature_service') + __slots__ = ('_settings', '_daa') - def __init__( - self, - *, - settings: HathorSettings, - daa: DifficultyAdjustmentAlgorithm, - feature_service: FeatureService, - ) -> None: + def __init__(self, *, settings: HathorSettings, daa: DifficultyAdjustmentAlgorithm) -> None: self._settings = settings self._daa = daa - self._feature_service = feature_service def verify_height(self, block: Block) -> None: """Validate that the block height is enough to confirm all transactions being confirmed.""" @@ -85,7 +78,7 @@ def verify_data(self, block: Block) -> None: def verify_mandatory_signaling(self, block: Block) -> None: """Verify whether this block is missing mandatory signaling for any feature.""" - signaling_state = self._feature_service.is_signaling_mandatory_features(block) + signaling_state = FeatureService.is_signaling_mandatory_features(block, self._settings) match signaling_state: case BlockIsSignaling(): diff --git a/hathor/verification/merge_mined_block_verifier.py b/hathor/verification/merge_mined_block_verifier.py index 60bfb42da..0fe9dba33 100644 --- a/hathor/verification/merge_mined_block_verifier.py +++ b/hathor/verification/merge_mined_block_verifier.py @@ -14,26 +14,21 @@ from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.transaction import MergeMinedBlock class MergeMinedBlockVerifier: - __slots__ = ('_settings', '_feature_service',) + __slots__ = ('_settings',) - def __init__(self, *, settings: HathorSettings, feature_service: FeatureService): + def __init__(self, *, settings: HathorSettings) -> None: self._settings = settings - self._feature_service = feature_service def verify_aux_pow(self, block: MergeMinedBlock) -> None: """ Verify auxiliary proof-of-work (for merged mining). """ assert block.aux_pow is not None - is_feature_active = self._feature_service.is_feature_active( - block=block, - feature=Feature.INCREASE_MAX_MERKLE_PATH_LENGTH - ) + is_feature_active = block.static_metadata.is_feature_active(Feature.INCREASE_MAX_MERKLE_PATH_LENGTH) max_merkle_path_length = ( self._settings.NEW_MAX_MERKLE_PATH_LENGTH if is_feature_active else self._settings.OLD_MAX_MERKLE_PATH_LENGTH diff --git a/hathor/verification/vertex_verifiers.py b/hathor/verification/vertex_verifiers.py index 1a9b56b21..0ecded117 100644 --- a/hathor/verification/vertex_verifiers.py +++ b/hathor/verification/vertex_verifiers.py @@ -16,7 +16,6 @@ from hathor.conf.settings import HathorSettings from hathor.daa import DifficultyAdjustmentAlgorithm -from hathor.feature_activation.feature_service import FeatureService from hathor.verification.block_verifier import BlockVerifier from hathor.verification.merge_mined_block_verifier import MergeMinedBlockVerifier from hathor.verification.poa_block_verifier import PoaBlockVerifier @@ -35,13 +34,7 @@ class VertexVerifiers(NamedTuple): token_creation_tx: TokenCreationTransactionVerifier @classmethod - def create_defaults( - cls, - *, - settings: HathorSettings, - daa: DifficultyAdjustmentAlgorithm, - feature_service: FeatureService, - ) -> 'VertexVerifiers': + def create_defaults(cls, *, settings: HathorSettings, daa: DifficultyAdjustmentAlgorithm) -> 'VertexVerifiers': """ Create a VertexVerifiers instance using the default verifier for each vertex type, from all required dependencies. @@ -52,7 +45,6 @@ def create_defaults( settings=settings, vertex_verifier=vertex_verifier, daa=daa, - feature_service=feature_service ) @classmethod @@ -62,13 +54,12 @@ def create( settings: HathorSettings, vertex_verifier: VertexVerifier, daa: DifficultyAdjustmentAlgorithm, - feature_service: FeatureService, ) -> 'VertexVerifiers': """ Create a VertexVerifiers instance using a custom vertex_verifier. """ - block_verifier = BlockVerifier(settings=settings, daa=daa, feature_service=feature_service) - merge_mined_block_verifier = MergeMinedBlockVerifier(settings=settings, feature_service=feature_service) + block_verifier = BlockVerifier(settings=settings, daa=daa) + merge_mined_block_verifier = MergeMinedBlockVerifier(settings=settings) poa_block_verifier = PoaBlockVerifier(settings=settings) tx_verifier = TransactionVerifier(settings=settings, daa=daa) token_creation_tx_verifier = TokenCreationTransactionVerifier(settings=settings) diff --git a/hathor/vertex_handler/vertex_handler.py b/hathor/vertex_handler/vertex_handler.py index 473516a56..5c1a32449 100644 --- a/hathor/vertex_handler/vertex_handler.py +++ b/hathor/vertex_handler/vertex_handler.py @@ -19,7 +19,6 @@ from hathor.conf.settings import HathorSettings from hathor.consensus import ConsensusAlgorithm from hathor.exception import HathorError, InvalidNewTransaction -from hathor.feature_activation.feature_service import FeatureService from hathor.p2p.manager import ConnectionsManager from hathor.pubsub import HathorEvents, PubSubManager from hathor.reactor import ReactorProtocol @@ -41,7 +40,6 @@ class VertexHandler: '_verification_service', '_consensus', '_p2p_manager', - '_feature_service', '_pubsub', '_wallet', '_log_vertex_bytes', @@ -56,7 +54,6 @@ def __init__( verification_service: VerificationService, consensus: ConsensusAlgorithm, p2p_manager: ConnectionsManager, - feature_service: FeatureService, pubsub: PubSubManager, wallet: BaseWallet | None, log_vertex_bytes: bool = False, @@ -68,7 +65,6 @@ def __init__( self._verification_service = verification_service self._consensus = consensus self._p2p_manager = p2p_manager - self._feature_service = feature_service self._pubsub = pubsub self._wallet = wallet self._log_vertex_bytes = log_vertex_bytes @@ -228,7 +224,7 @@ def _log_new_object(self, tx: BaseTransaction, message_fmt: str, *, quiet: bool) if tx.is_block: message = message_fmt.format('block') if isinstance(tx, Block): - feature_infos = self._feature_service.get_feature_infos(block=tx) + feature_infos = tx.static_metadata.get_feature_infos(self._settings) feature_states = { feature.value: info.state.value for feature, info in feature_infos.items() diff --git a/tests/feature_activation/test_bit_signaling_service.py b/tests/feature_activation/test_bit_signaling_service.py index 8b487be92..20fa3f726 100644 --- a/tests/feature_activation/test_bit_signaling_service.py +++ b/tests/feature_activation/test_bit_signaling_service.py @@ -16,16 +16,19 @@ import pytest +from hathor.conf.get_settings import get_global_settings from hathor.conf.settings import HathorSettings from hathor.feature_activation.bit_signaling_service import BitSignalingService from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.model.criteria import Criteria 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.pubsub import HathorEvents, PubSubManager from hathor.transaction import Block +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.storage import TransactionStorage +from tests.test_memory_reactor_clock import TestMemoryReactorClock @pytest.mark.parametrize( @@ -168,19 +171,19 @@ def _test_generate_signal_bits( ) -> int: settings = Mock(spec_set=HathorSettings) settings.FEATURE_ACTIVATION = FeatureSettings() - feature_service = Mock(spec_set=FeatureService) - feature_service.get_feature_infos = lambda block: feature_infos + block = Mock(spec_set=Block) + block.static_metadata.get_feature_infos = lambda _: feature_infos service = BitSignalingService( settings=settings, - feature_service=feature_service, tx_storage=Mock(), support_features=support_features, not_support_features=not_support_features, feature_storage=Mock(), + pubsub=Mock(), ) - return service.generate_signal_bits(block=Mock()) + return service.generate_signal_bits(block=block) @pytest.mark.parametrize( @@ -216,11 +219,11 @@ def test_support_intersection_validation( with pytest.raises(ValueError) as e: BitSignalingService( settings=Mock(), - feature_service=Mock(), tx_storage=Mock(), support_features=support_features, not_support_features=not_support_features, feature_storage=Mock(), + pubsub=Mock(), ) message = str(e.value) @@ -258,27 +261,24 @@ def test_non_signaling_features_warning( settings = Mock(spec_set=HathorSettings) settings.FEATURE_ACTIVATION = FeatureSettings() - best_block = Mock(spec_set=Block) - best_block.get_height = Mock(return_value=123) - best_block.hash_hex = 'abc' + best_block = Block(hash=b'abc') + static_metadata = BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[], + feature_states={}, + ) + best_block.set_static_metadata(static_metadata) 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: - return {} - raise NotImplementedError - - feature_service = Mock(spec_set=FeatureService) - feature_service.get_feature_infos = get_feature_infos_mock - service = BitSignalingService( settings=settings, - feature_service=feature_service, tx_storage=tx_storage, support_features=support_features, not_support_features=not_support_features, feature_storage=Mock(), + pubsub=Mock(), ) logger_mock = Mock() service._log = logger_mock @@ -289,38 +289,124 @@ def get_feature_infos_mock(block: Block) -> dict[Feature, FeatureInfo]: 'Considering the current best block, there are signaled features outside their signaling period. ' 'Therefore, signaling for them has no effect. Make sure you are signaling for the desired features.', best_block_height=123, - best_block_hash='abc', + best_block_hash=b'abc'.hex(), non_signaling_features=non_signaling_features, ) def test_on_must_signal_not_supported() -> None: + settings = get_global_settings()._replace( + FEATURE_ACTIVATION=FeatureSettings.construct( + features={ + Feature.NOP_FEATURE_1: Mock() + } + ) + ) + + best_block = Mock(spec_set=Block) + best_block.static_metadata.get_feature_infos = Mock(return_value={}) + + storage = Mock(spec_set=TransactionStorage) + storage.get_best_block = Mock(return_value=best_block) + + pubsub = PubSubManager(TestMemoryReactorClock()) service = BitSignalingService( - settings=Mock(), - feature_service=Mock(), - tx_storage=Mock(), + settings=settings, + tx_storage=storage, support_features=set(), not_support_features={Feature.NOP_FEATURE_1}, feature_storage=Mock(), + pubsub=pubsub, + ) + + parent_block = Block() + parent_block.set_static_metadata( + BlockStaticMetadata( + height=122, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ + Feature.NOP_FEATURE_1: FeatureState.STARTED + } + ) + ) + + block = Block() + block.set_static_metadata( + BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ + Feature.NOP_FEATURE_1: FeatureState.MUST_SIGNAL + } + ) ) + block_mock = Mock(wraps=block, spec_set=Block) + block_mock.get_block_parent = Mock(return_value=parent_block) + block_mock.is_genesis = False - service.on_must_signal(feature=Feature.NOP_FEATURE_1) + service.start() + pubsub.publish(HathorEvents.NETWORK_NEW_TX_ACCEPTED, tx=block_mock) assert service._support_features == {Feature.NOP_FEATURE_1} assert service._not_support_features == set() def test_on_must_signal_supported() -> None: + settings = get_global_settings()._replace( + FEATURE_ACTIVATION=FeatureSettings.construct( + features={ + Feature.NOP_FEATURE_1: Mock() + } + ) + ) + + best_block = Mock(spec_set=Block) + best_block.static_metadata.get_feature_infos = Mock(return_value={}) + + storage = Mock(spec_set=TransactionStorage) + storage.get_best_block = Mock(return_value=best_block) + + pubsub = PubSubManager(TestMemoryReactorClock()) service = BitSignalingService( - settings=Mock(), - feature_service=Mock(), - tx_storage=Mock(), + settings=settings, + tx_storage=storage, support_features=set(), not_support_features=set(), feature_storage=Mock(), + pubsub=pubsub, + ) + + parent_block = Block() + parent_block.set_static_metadata( + BlockStaticMetadata( + height=122, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ + Feature.NOP_FEATURE_1: FeatureState.STARTED + } + ) ) - service.on_must_signal(feature=Feature.NOP_FEATURE_1) + block = Block() + block.set_static_metadata( + BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ + Feature.NOP_FEATURE_1: FeatureState.MUST_SIGNAL + } + ) + ) + block_mock = Mock(wraps=block, spec_set=Block) + block_mock.get_block_parent = Mock(return_value=parent_block) + block_mock.is_genesis = False + + service.start() + pubsub.publish(HathorEvents.NETWORK_NEW_TX_ACCEPTED, tx=block_mock) assert service._support_features == {Feature.NOP_FEATURE_1} assert service._not_support_features == set() diff --git a/tests/feature_activation/test_feature_service.py b/tests/feature_activation/test_feature_service.py index a1c2f1549..3aad37fd3 100644 --- a/tests/feature_activation/test_feature_service.py +++ b/tests/feature_activation/test_feature_service.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import Mock, patch +from unittest.mock import Mock import pytest @@ -31,6 +31,7 @@ from hathor.feature_activation.settings import Settings as FeatureSettings from hathor.indexes import MemoryIndexesManager from hathor.transaction import Block +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.storage import TransactionMemoryStorage, TransactionStorage from hathor.transaction.validation_state import ValidationState from hathor.util import not_none @@ -98,25 +99,33 @@ def get_settings(*, features: dict[Feature, Criteria]) -> HathorSettings: return settings -def test_get_state_genesis() -> None: +def get_feature_service(settings: HathorSettings, storage: TransactionStorage) -> FeatureService: + return FeatureService( + settings=settings, + vertex_getter=storage.get_vertex, + block_by_height_getter=storage.get_block_by_height, + ) + + +def test_calculate_state_genesis() -> None: settings = get_settings(features={}) storage = get_storage(settings, up_to_height=0) - service = FeatureService(settings=settings, tx_storage=storage) + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(0)) - result = service.get_state(block=block, feature=Mock()) + result = service._calculate_state(block=block, height=0, feature=Mock()) assert result == FeatureState.DEFINED @pytest.mark.parametrize('block_height', [0, 1, 2, 3]) -def test_get_state_first_interval(block_height: int) -> None: +def test_calculate_state_first_interval(block_height: int) -> None: settings = get_settings(features={ Feature.NOP_FEATURE_1: Mock() }) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.DEFINED @@ -130,7 +139,7 @@ def test_get_state_first_interval(block_height: int) -> None: (8, FeatureState.DEFINED) ] ) -def test_get_state_from_defined(block_height: int, start_height: int, expected_state: FeatureState) -> None: +def test_calculate_state_from_defined(block_height: int, start_height: int, expected_state: FeatureState) -> None: features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=Mock(), @@ -141,18 +150,17 @@ def test_get_state_from_defined(block_height: int, start_height: int, expected_s } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == expected_state @pytest.mark.parametrize('block_height', [12, 13, 14, 15, 16, 17]) @pytest.mark.parametrize('timeout_height', [8, 12]) -def test_get_state_from_started_to_failed( +def test_calculate_state_from_started_to_failed( block_height: int, timeout_height: int, ) -> None: @@ -167,18 +175,17 @@ def test_get_state_from_started_to_failed( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.FAILED @pytest.mark.parametrize('block_height', [8, 9, 10, 11]) @pytest.mark.parametrize('timeout_height', [8, 12]) -def test_get_state_from_started_to_must_signal_on_timeout( +def test_calculate_state_from_started_to_must_signal_on_timeout( block_height: int, timeout_height: int, ) -> None: @@ -193,19 +200,17 @@ def test_get_state_from_started_to_must_signal_on_timeout( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.MUST_SIGNAL - service.bit_signaling_service.on_must_signal.assert_called_once_with(Feature.NOP_FEATURE_1) @pytest.mark.parametrize('block_height', [8, 9, 10, 11]) @pytest.mark.parametrize('default_threshold', [0, 1, 2, 3]) -def test_get_state_from_started_to_locked_in_on_default_threshold( +def test_calculate_state_from_started_to_locked_in_on_default_threshold( block_height: int, default_threshold: int ) -> None: @@ -224,18 +229,17 @@ def test_get_state_from_started_to_locked_in_on_default_threshold( ) settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.LOCKED_IN @pytest.mark.parametrize('block_height', [8, 9, 10, 11]) @pytest.mark.parametrize('custom_threshold', [0, 1, 2, 3]) -def test_get_state_from_started_to_locked_in_on_custom_threshold( +def test_calculate_state_from_started_to_locked_in_on_custom_threshold( block_height: int, custom_threshold: int ) -> None: @@ -250,11 +254,10 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.LOCKED_IN @@ -268,7 +271,7 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( (True, 20), ] ) -def test_get_state_from_started_to_started( +def test_calculate_state_from_started_to_started( block_height: int, lock_in_on_timeout: bool, timeout_height: int, @@ -284,17 +287,16 @@ def test_get_state_from_started_to_started( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.STARTED @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) -def test_get_state_from_must_signal_to_locked_in( +def test_calculate_state_from_must_signal_to_locked_in( block_height: int, ) -> None: features = { @@ -308,18 +310,17 @@ def test_get_state_from_must_signal_to_locked_in( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.LOCKED_IN @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) @pytest.mark.parametrize('minimum_activation_height', [0, 4, 8, 12, 16]) -def test_get_state_from_locked_in_to_active( +def test_calculate_state_from_locked_in_to_active( block_height: int, minimum_activation_height: int, ) -> None: @@ -335,18 +336,17 @@ def test_get_state_from_locked_in_to_active( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.ACTIVE @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) @pytest.mark.parametrize('minimum_activation_height', [17, 20, 100]) -def test_get_state_from_locked_in_to_locked_in( +def test_calculate_state_from_locked_in_to_locked_in( block_height: int, minimum_activation_height: int, ) -> None: @@ -362,17 +362,16 @@ def test_get_state_from_locked_in_to_locked_in( } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.LOCKED_IN @pytest.mark.parametrize('block_height', [20, 21, 22, 23]) -def test_get_state_from_active(block_height: int) -> None: +def test_calculate_state_from_active(block_height: int) -> None: features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, @@ -384,11 +383,10 @@ def test_get_state_from_active(block_height: int) -> None: } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.ACTIVE @@ -406,20 +404,15 @@ def test_is_feature_active(block_height: int) -> None: } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService( - settings=settings, - tx_storage=storage - ) - 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 = block.static_metadata.is_feature_active(Feature.NOP_FEATURE_1) assert result is True @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) -def test_get_state_from_failed(block_height: int) -> None: +def test_calculate_state_from_failed(block_height: int) -> None: features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=Mock(), @@ -430,48 +423,43 @@ def test_get_state_from_failed(block_height: int) -> None: } settings = get_settings(features=features) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() + service = get_feature_service(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.FAILED -def test_get_state_undefined_feature() -> None: +def test_calculate_state_undefined_feature() -> None: settings = get_settings(features={}) storage = get_storage(settings, up_to_height=10) block = not_none(storage.get_block_by_height(10)) - service = FeatureService(settings=settings, tx_storage=storage) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + service = get_feature_service(settings, storage) + result = service._calculate_state(block=block, height=10, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.DEFINED def test_get_feature_info() -> None: - criteria_mock_1 = Criteria.construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) - criteria_mock_2 = Criteria.construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) + criteria_mock_1 = Mock() + criteria_mock_2 = Mock() settings = get_settings(features={ Feature.NOP_FEATURE_1: criteria_mock_1, Feature.NOP_FEATURE_2: criteria_mock_2 }) - storage = get_storage(settings, up_to_height=0) - service = FeatureService( - settings=settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() - - def get_state(self: FeatureService, *, block: Block, feature: Feature) -> FeatureState: - states = { + block = Block() + static_metadata = BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ Feature.NOP_FEATURE_1: FeatureState.STARTED, - Feature.NOP_FEATURE_2: FeatureState.FAILED + Feature.NOP_FEATURE_2: FeatureState.FAILED, } - return states[feature] - - with patch('hathor.feature_activation.feature_service.FeatureService.get_state', get_state): - result = service.get_feature_infos(block=Mock()) + ) + block.set_static_metadata(static_metadata) + result = block.static_metadata.get_feature_infos(settings) expected = { Feature.NOP_FEATURE_1: FeatureInfo(criteria_mock_1, FeatureState.STARTED), @@ -481,92 +469,6 @@ def get_state(self: FeatureService, *, block: Block, feature: Feature) -> Featur assert result == expected -@pytest.mark.parametrize( - ['block_height', 'ancestor_height'], - [ - (21, 21), - (21, 100), - (10, 15), - (10, 11), - (0, 0), - ] -) -def test_get_ancestor_at_height_invalid(block_height: int, ancestor_height: int) -> None: - settings = get_settings(features={}) - storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() - block = not_none(storage.get_block_by_height(block_height)) - - with pytest.raises(AssertionError) as e: - service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) - - assert str(e.value) == ( - f"ancestor height must be lower than the block's height: {ancestor_height} >= {block_height}" - ) - - -@pytest.mark.parametrize( - ['block_height', 'ancestor_height'], - [ - (21, 20), - (21, 18), - (21, 17), - (15, 12), - (15, 11), - (1, 0), - ] -) -def test_get_ancestor_at_height(block_height: int, ancestor_height: int) -> None: - settings = get_settings(features={}) - storage = get_storage(settings, up_to_height=block_height) - block = not_none(storage.get_block_by_height(block_height)) - - get_block_by_height_wrapped = Mock(wraps=storage.get_block_by_height) - with patch.object(storage, 'get_block_by_height', get_block_by_height_wrapped): - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() - result = service._get_ancestor_at_height( - block=block, - ancestor_height=ancestor_height - ) - - assert get_block_by_height_wrapped.call_count == ( - 0 if block_height - ancestor_height <= 1 else 1 - ), 'this should only be called if the ancestor is deeper than one parent away' - assert result == storage.get_block_by_height(ancestor_height) - assert result.get_height() == ancestor_height - - -@pytest.mark.parametrize( - ['block_height', 'ancestor_height'], - [ - (21, 20), - (21, 18), - (15, 12), - (15, 11), - (1, 0), - ] -) -def test_get_ancestor_at_height_voided(block_height: int, ancestor_height: int) -> None: - settings = get_settings(features={}) - storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() - block = not_none(storage.get_block_by_height(block_height)) - - parent_block = not_none(storage.get_block_by_height(block_height - 1)) - parent_block.get_metadata().voided_by = {b'some'} - - get_block_by_height_wrapped = Mock(wraps=storage.get_block_by_height) - with patch.object(storage, 'get_block_by_height', get_block_by_height_wrapped): - result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) - - assert get_block_by_height_wrapped.call_count == 0 - assert result == storage.get_block_by_height(ancestor_height) - assert result.get_height() == ancestor_height - - @pytest.mark.parametrize( ['bit', 'threshold', 'block_height', 'signaling_state'], [ @@ -614,10 +516,8 @@ def test_check_must_signal( ) settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) storage = get_storage(settings, up_to_height=block_height) - service = FeatureService(settings=settings, tx_storage=storage) - service.bit_signaling_service = Mock() block = not_none(storage.get_block_by_height(block_height)) - result = service.is_signaling_mandatory_features(block) + result = FeatureService.is_signaling_mandatory_features(block, settings) assert result == signaling_state diff --git a/tests/feature_activation/test_feature_simulation.py b/tests/feature_activation/test_feature_simulation.py index 5aac3a4de..f9c4bcd9f 100644 --- a/tests/feature_activation/test_feature_simulation.py +++ b/tests/feature_activation/test_feature_simulation.py @@ -70,16 +70,14 @@ def test_feature(self) -> None: settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) builder = self.get_simulator_builder().set_settings(settings) artifacts = self.simulator.create_artifacts(builder) - feature_service = artifacts.feature_service manager = artifacts.manager - feature_resource = FeatureResource( - settings=settings, - feature_service=feature_service, - tx_storage=artifacts.tx_storage - ) + feature_resource = FeatureResource(settings=settings, tx_storage=artifacts.tx_storage) web_client = StubSite(feature_resource) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at the beginning, the feature is DEFINED: add_new_blocks(manager, 10) self.simulator.run(60) @@ -101,6 +99,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at block 19, the feature is DEFINED, just before becoming STARTED: add_new_blocks(manager, 9) self.simulator.run(60) @@ -122,6 +123,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at block 20, the feature becomes STARTED: add_new_blocks(manager, 1) self.simulator.run(60) @@ -143,6 +147,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + # at block 55, the feature is STARTED, just before becoming MUST_SIGNAL: add_new_blocks(manager, 35) self.simulator.run(60) @@ -164,6 +171,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + # at block 56, the feature becomes MUST_SIGNAL: add_new_blocks(manager, 1) self.simulator.run(60) @@ -185,6 +195,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [Feature.NOP_FEATURE_1] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + add_new_blocks(manager, 1, signal_bits=0b1) # if we try to propagate a non-signaling block, it is not accepted @@ -219,6 +232,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [Feature.NOP_FEATURE_1] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at block 60, the feature becomes LOCKED_IN: add_new_blocks(manager, 1) self.simulator.run(60) @@ -240,6 +256,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [Feature.NOP_FEATURE_1] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at block 71, the feature is LOCKED_IN, just before becoming ACTIVE: add_new_blocks(manager, 11) self.simulator.run(60) @@ -261,6 +280,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [Feature.NOP_FEATURE_1] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at block 72, the feature becomes ACTIVE, forever: add_new_blocks(manager, 1) self.simulator.run(60) @@ -282,6 +304,9 @@ def test_feature(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + def test_reorg(self) -> None: feature_settings = FeatureSettings( evaluation_interval=4, @@ -301,16 +326,14 @@ def test_reorg(self) -> None: settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) builder = self.get_simulator_builder().set_settings(settings) artifacts = self.simulator.create_artifacts(builder) - feature_service = artifacts.feature_service manager = artifacts.manager - feature_resource = FeatureResource( - settings=settings, - feature_service=feature_service, - tx_storage=artifacts.tx_storage - ) + feature_resource = FeatureResource(settings=settings, tx_storage=artifacts.tx_storage) web_client = StubSite(feature_resource) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at the beginning, the feature is DEFINED: self.simulator.run(60) result = self._get_result(web_client) @@ -331,6 +354,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # at block 4, the feature becomes STARTED with 0% acceptance add_new_blocks(manager, 4) self.simulator.run(60) @@ -352,6 +378,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + # at block 7, acceptance is 25% (we're signaling 1 block out of 4) add_new_blocks(manager, 2) add_new_blocks(manager, 1, signal_bits=0b10) @@ -374,6 +403,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + # at block 11, acceptance is 75% (we're signaling 3 blocks out of 4), # so the feature will be locked-in in the next block add_new_blocks(manager, 1) @@ -397,6 +429,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + # at block 12, the feature is locked-in add_new_blocks(manager, 1) self.simulator.run(60) @@ -418,6 +453,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + # at block 16, the feature is activated add_new_blocks(manager, 4) self.simulator.run(60) @@ -439,6 +477,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [] + # We then create a new manager with one more block (17 vs 16), so its blockchain wins when # both managers are connected. This causes a reorg and the feature goes back to the STARTED state. builder2 = self.get_simulator_builder().set_settings(settings) @@ -470,6 +511,9 @@ def test_reorg(self) -> None: ] ) + assert artifacts.bit_signaling_service.get_support_features() == [] + assert artifacts.bit_signaling_service.get_not_support_features() == [Feature.NOP_FEATURE_1] + class BaseMemoryStorageFeatureSimulationTest(BaseFeatureSimulationTest): def get_simulator_builder(self) -> Builder: @@ -516,14 +560,9 @@ def test_feature_from_existing_storage(self) -> None: rocksdb_dir = self.get_rocksdb_directory() builder1 = self.get_simulator_builder_from_dir(rocksdb_dir).set_settings(settings) artifacts1 = self.simulator.create_artifacts(builder1) - feature_service1 = artifacts1.feature_service manager1 = artifacts1.manager - feature_resource = FeatureResource( - settings=settings, - feature_service=feature_service1, - tx_storage=artifacts1.tx_storage - ) + feature_resource = FeatureResource(settings=settings, tx_storage=artifacts1.tx_storage) web_client = StubSite(feature_resource) assert artifacts1.tx_storage.get_vertices_count() == 3 # genesis vertices in the storage @@ -557,13 +596,8 @@ def test_feature_from_existing_storage(self) -> None: # new builder is created with the same storage from the previous manager builder2 = self.get_simulator_builder_from_dir(rocksdb_dir).set_settings(settings) artifacts2 = self.simulator.create_artifacts(builder2) - feature_service = artifacts2.feature_service - feature_resource = FeatureResource( - settings=settings, - feature_service=feature_service, - tx_storage=artifacts2.tx_storage - ) + feature_resource = FeatureResource(settings=settings, tx_storage=artifacts2.tx_storage) web_client = StubSite(feature_resource) # the new storage starts populated diff --git a/tests/others/test_cli_builder.py b/tests/others/test_cli_builder.py index a83f00899..e6314baa7 100644 --- a/tests/others/test_cli_builder.py +++ b/tests/others/test_cli_builder.py @@ -1,5 +1,3 @@ -from unittest.mock import Mock - import pytest from hathor.builder import CliBuilder, ResourcesBuilder @@ -32,7 +30,7 @@ def _build_with_error(self, cmd_args: list[str], err_msg: str) -> None: builder = CliBuilder(args) with self.assertRaises(BuilderError) as cm: manager = builder.create_manager(self.reactor) - self.resources_builder = ResourcesBuilder(manager, args, builder.event_ws_factory, Mock()) + self.resources_builder = ResourcesBuilder(manager, args, builder.event_ws_factory) self.resources_builder.build() self.assertEqual(err_msg, str(cm.exception)) @@ -42,7 +40,7 @@ def _build(self, cmd_args: list[str]) -> HathorManager: builder = CliBuilder(args) manager = builder.create_manager(self.reactor) self.assertIsNotNone(manager) - self.resources_builder = ResourcesBuilder(manager, args, builder.event_ws_factory, Mock()) + self.resources_builder = ResourcesBuilder(manager, args, builder.event_ws_factory) self.resources_builder.build() return manager diff --git a/tests/resources/feature/test_feature.py b/tests/resources/feature/test_feature.py index fec031dc9..ddde41b6b 100644 --- a/tests/resources/feature/test_feature.py +++ b/tests/resources/feature/test_feature.py @@ -16,11 +16,9 @@ import pytest -from hathor.conf.get_settings import get_global_settings +from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.model.criteria import Criteria -from hathor.feature_activation.model.feature_info import FeatureInfo from hathor.feature_activation.model.feature_state import FeatureState from hathor.feature_activation.resources.feature import FeatureResource from hathor.feature_activation.settings import Settings as FeatureSettings @@ -37,7 +35,10 @@ def web() -> StubSite: height=123, min_height=0, feature_activation_bit_counts=[0, 1, 0, 0], - feature_states={}, + feature_states={ + Feature.NOP_FEATURE_1: FeatureState.DEFINED, + Feature.NOP_FEATURE_2: FeatureState.LOCKED_IN, + }, ) block.set_static_metadata(static_metadata) @@ -45,44 +46,29 @@ def web() -> StubSite: tx_storage.get_best_block = Mock(return_value=block) tx_storage.get_transaction = Mock(return_value=block) - def get_state(*, block: Block, feature: Feature) -> FeatureState: - return FeatureState.ACTIVE if feature is Feature.NOP_FEATURE_1 else FeatureState.STARTED - - nop_feature_1_criteria = Criteria( - bit=0, - start_height=0, - timeout_height=100, - version='0.1.0' - ) - nop_feature_2_criteria = Criteria( - bit=1, - start_height=200, - threshold=2, - timeout_height=300, - version='0.2.0' - ) - - feature_service = Mock(spec_set=FeatureService) - feature_service.get_state = Mock(side_effect=get_state) - feature_service.get_feature_infos = Mock(return_value={ - Feature.NOP_FEATURE_1: FeatureInfo(state=FeatureState.DEFINED, criteria=nop_feature_1_criteria), - Feature.NOP_FEATURE_2: FeatureInfo(state=FeatureState.LOCKED_IN, criteria=nop_feature_2_criteria), - }) - - settings = get_global_settings()._replace( - FEATURE_ACTIVATION=FeatureSettings( - evaluation_interval=4, - default_threshold=3, - features={ - Feature.NOP_FEATURE_1: nop_feature_1_criteria, - Feature.NOP_FEATURE_2: nop_feature_2_criteria - } - ) + settings = Mock(spec_set=HathorSettings) + settings.FEATURE_ACTIVATION = FeatureSettings( + evaluation_interval=4, + default_threshold=3, + features={ + Feature.NOP_FEATURE_1: Criteria( + bit=0, + start_height=0, + timeout_height=100, + version='0.1.0' + ), + Feature.NOP_FEATURE_2: Criteria( + bit=1, + start_height=200, + threshold=2, + timeout_height=300, + version='0.2.0' + ), + } ) feature_resource = FeatureResource( settings=settings, - feature_service=feature_service, tx_storage=tx_storage ) diff --git a/tests/resources/transaction/test_mining.py b/tests/resources/transaction/test_mining.py index 80ba0bbaa..77132a554 100644 --- a/tests/resources/transaction/test_mining.py +++ b/tests/resources/transaction/test_mining.py @@ -40,7 +40,8 @@ def test_get_block_template_with_address(self): 'height': 1, 'min_height': 0, 'first_block': None, - 'feature_activation_bit_counts': [0, 0, 0, 0] + 'feature_activation_bit_counts': [0, 0, 0, 0], + 'feature_states': {}, }, 'tokens': [], 'data': '', @@ -73,7 +74,8 @@ def test_get_block_template_without_address(self): 'height': 1, 'min_height': 0, 'first_block': None, - 'feature_activation_bit_counts': [0, 0, 0, 0] + 'feature_activation_bit_counts': [0, 0, 0, 0], + 'feature_states': {}, }, 'tokens': [], 'data': '', diff --git a/tests/tx/test_block.py b/tests/tx/test_block.py index 1e103ed2f..b297c2c5c 100644 --- a/tests/tx/test_block.py +++ b/tests/tx/test_block.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest from hathor.conf.get_settings import get_global_settings -from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature from hathor.feature_activation.feature_service import BlockIsMissingSignal, BlockIsSignaling, FeatureService from hathor.indexes import MemoryIndexesManager @@ -139,25 +138,23 @@ def test_get_feature_activation_bit_value() -> None: def test_verify_must_signal() -> None: - settings = Mock(spec_set=HathorSettings) - feature_service = Mock(spec_set=FeatureService) - feature_service.is_signaling_mandatory_features = Mock( + verifier = BlockVerifier(settings=Mock(), daa=Mock()) + is_signaling_mandatory_features_mock = Mock( return_value=BlockIsMissingSignal(feature=Feature.NOP_FEATURE_1) ) - verifier = BlockVerifier(settings=settings, feature_service=feature_service, daa=Mock()) block = Block() - with pytest.raises(BlockMustSignalError) as e: - verifier.verify_mandatory_signaling(block) + with patch.object(FeatureService, 'is_signaling_mandatory_features', is_signaling_mandatory_features_mock): + with pytest.raises(BlockMustSignalError) as e: + verifier.verify_mandatory_signaling(block) assert str(e.value) == "Block must signal support for feature 'NOP_FEATURE_1' during MUST_SIGNAL phase." def test_verify_must_not_signal() -> None: - settings = Mock(spec_set=HathorSettings) - feature_service = Mock(spec_set=FeatureService) - feature_service.is_signaling_mandatory_features = Mock(return_value=BlockIsSignaling()) - verifier = BlockVerifier(settings=settings, feature_service=feature_service, daa=Mock()) + verifier = BlockVerifier(settings=Mock(), daa=Mock()) + is_signaling_mandatory_features_mock = Mock(return_value=BlockIsSignaling()) block = Block() - verifier.verify_mandatory_signaling(block) + with patch.object(FeatureService, 'is_signaling_mandatory_features', is_signaling_mandatory_features_mock): + verifier.verify_mandatory_signaling(block) diff --git a/tests/tx/test_genesis.py b/tests/tx/test_genesis.py index f2839bf6b..5fefdf7f8 100644 --- a/tests/tx/test_genesis.py +++ b/tests/tx/test_genesis.py @@ -32,7 +32,7 @@ class GenesisTest(unittest.TestCase): def setUp(self) -> None: super().setUp() self._daa = DifficultyAdjustmentAlgorithm(settings=self._settings) - verifiers = VertexVerifiers.create_defaults(settings=self._settings, daa=self._daa, feature_service=Mock()) + verifiers = VertexVerifiers.create_defaults(settings=self._settings, daa=self._daa) self._verification_service = VerificationService(settings=self._settings, verifiers=verifiers) self.storage = TransactionMemoryStorage(settings=settings) diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index 48e1ad6e8..561941fc7 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -1,7 +1,6 @@ import base64 import hashlib from math import isinf, isnan -from unittest.mock import patch import pytest @@ -9,7 +8,7 @@ from hathor.daa import TestMode from hathor.exception import InvalidNewTransaction from hathor.feature_activation.feature import Feature -from hathor.feature_activation.feature_service import FeatureService +from hathor.feature_activation.model.feature_state import FeatureState from hathor.simulator.utils import add_new_blocks from hathor.transaction import MAX_OUTPUT_VALUE, Block, Transaction, TxInput, TxOutput from hathor.transaction.exceptions import ( @@ -34,6 +33,7 @@ WeightError, ) from hathor.transaction.scripts import P2PKH, parse_address_script +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.util import int_to_bytes from hathor.transaction.validation_state import ValidationState from hathor.wallet import Wallet @@ -341,16 +341,6 @@ def test_merge_mined_long_merkle_path(self): address = decode_address(self.get_address(1)) outputs = [TxOutput(100, P2PKH.create_output_script(address))] - patch_path = 'hathor.feature_activation.feature_service.FeatureService.is_feature_active' - - def is_feature_active_false(self: FeatureService, *, block: Block, feature: Feature) -> bool: - assert feature == Feature.INCREASE_MAX_MERKLE_PATH_LENGTH - return False - - def is_feature_active_true(self: FeatureService, *, block: Block, feature: Feature) -> bool: - assert feature == Feature.INCREASE_MAX_MERKLE_PATH_LENGTH - return True - b = MergeMinedBlock( timestamp=self.genesis_blocks[0].timestamp + 1, weight=1, @@ -364,16 +354,24 @@ def is_feature_active_true(self: FeatureService, *, block: Block, feature: Featu b'\x00' * 12, ) ) + static_metadata = BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ + Feature.INCREASE_MAX_MERKLE_PATH_LENGTH: FeatureState.FAILED + }, + ) + b.set_static_metadata(static_metadata) # Test with the INCREASE_MAX_MERKLE_PATH_LENGTH feature disabled - with patch(patch_path, is_feature_active_false): - with self.assertRaises(AuxPowLongMerklePathError): - self._verifiers.merge_mined_block.verify_aux_pow(b) - - # removing one path makes it work - b.aux_pow.merkle_path.pop() + with self.assertRaises(AuxPowLongMerklePathError): self._verifiers.merge_mined_block.verify_aux_pow(b) + # removing one path makes it work + b.aux_pow.merkle_path.pop() + self._verifiers.merge_mined_block.verify_aux_pow(b) + b2 = MergeMinedBlock( timestamp=self.genesis_blocks[0].timestamp + 1, weight=1, @@ -387,16 +385,24 @@ def is_feature_active_true(self: FeatureService, *, block: Block, feature: Featu b'\x00' * 12, ) ) + static_metadata = BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[], + feature_states={ + Feature.INCREASE_MAX_MERKLE_PATH_LENGTH: FeatureState.ACTIVE + }, + ) + b2.set_static_metadata(static_metadata) # Test with the INCREASE_MAX_MERKLE_PATH_LENGTH feature enabled - with patch(patch_path, is_feature_active_true): - with self.assertRaises(AuxPowLongMerklePathError): - self._verifiers.merge_mined_block.verify_aux_pow(b2) - - # removing one path makes it work - b2.aux_pow.merkle_path.pop() + with self.assertRaises(AuxPowLongMerklePathError): self._verifiers.merge_mined_block.verify_aux_pow(b2) + # removing one path makes it work + b2.aux_pow.merkle_path.pop() + self._verifiers.merge_mined_block.verify_aux_pow(b2) + def test_block_outputs(self): from hathor.transaction.exceptions import TooManyOutputs diff --git a/tests/tx/test_tx_deserialization.py b/tests/tx/test_tx_deserialization.py index f467c26f0..54ea7fbef 100644 --- a/tests/tx/test_tx_deserialization.py +++ b/tests/tx/test_tx_deserialization.py @@ -1,5 +1,3 @@ -from unittest.mock import Mock - from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.transaction import Block, MergeMinedBlock, Transaction, TxVersion from hathor.transaction.token_creation_tx import TokenCreationTransaction @@ -13,7 +11,7 @@ class _DeserializationTest(unittest.TestCase): def setUp(self) -> None: super().setUp() daa = DifficultyAdjustmentAlgorithm(settings=self._settings) - verifiers = VertexVerifiers.create_defaults(settings=self._settings, daa=daa, feature_service=Mock()) + verifiers = VertexVerifiers.create_defaults(settings=self._settings, daa=daa) self._verification_service = VerificationService(settings=self._settings, verifiers=verifiers) def test_deserialize(self):