diff --git a/hathor/builder/builder.py b/hathor/builder/builder.py index 6da1d3445..8beac255b 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 @@ -71,7 +70,6 @@ class BuildArtifacts(NamedTuple): pubsub: PubSubManager consensus: ConsensusAlgorithm tx_storage: TransactionStorage - feature_service: FeatureService bit_signaling_service: BitSignalingService indexes: Optional[IndexesManager] wallet: Optional[BaseWallet] @@ -79,10 +77,7 @@ class BuildArtifacts(NamedTuple): stratum_factory: Optional[StratumFactory] -_VertexVerifiersBuilder: TypeAlias = Callable[ - [HathorSettingsType, DifficultyAdjustmentAlgorithm, FeatureService], - VertexVerifiers -] +_VertexVerifiersBuilder: TypeAlias = Callable[[HathorSettingsType, DifficultyAdjustmentAlgorithm], VertexVerifiers] class Builder: @@ -115,7 +110,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 @@ -183,7 +177,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() @@ -251,7 +244,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 ) @@ -266,11 +258,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 @@ -413,6 +400,7 @@ def _get_or_create_indexes_manager(self) -> IndexesManager: def _get_or_create_tx_storage(self) -> TransactionStorage: indexes = self._get_or_create_indexes_manager() + settings = self._get_or_create_settings() if self._tx_storage is not None: # If a tx storage is provided, set the indexes manager to it. @@ -424,11 +412,11 @@ def _get_or_create_tx_storage(self) -> TransactionStorage: store_indexes = None if self._storage_type == StorageType.MEMORY: - self._tx_storage = TransactionMemoryStorage(indexes=store_indexes) + self._tx_storage = TransactionMemoryStorage(indexes=store_indexes, settings=settings) elif self._storage_type == StorageType.ROCKSDB: rocksdb_storage = self._get_or_create_rocksdb_storage() - self._tx_storage = TransactionRocksDBStorage(rocksdb_storage, indexes=store_indexes) + self._tx_storage = TransactionRocksDBStorage(rocksdb_storage, indexes=store_indexes, settings=settings) else: raise NotImplementedError @@ -438,7 +426,13 @@ def _get_or_create_tx_storage(self) -> TransactionStorage: kwargs: dict[str, Any] = {} if self._tx_storage_cache_capacity is not None: kwargs['capacity'] = self._tx_storage_cache_capacity - self._tx_storage = TransactionCacheStorage(self._tx_storage, reactor, indexes=indexes, **kwargs) + self._tx_storage = TransactionCacheStorage( + self._tx_storage, + reactor, + indexes=indexes, + settings=settings, + **kwargs + ) return self._tx_storage @@ -477,27 +471,13 @@ 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( - feature_settings=settings.FEATURE_ACTIVATION, - 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() self._bit_signaling_service = BitSignalingService( - feature_settings=settings.FEATURE_ACTIVATION, - feature_service=feature_service, + settings=settings, tx_storage=tx_storage, support_features=self._support_features, not_support_features=self._not_support_features, @@ -531,17 +511,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 @@ -567,7 +542,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 f38160b2c..3afe697b2 100644 --- a/hathor/builder/cli_builder.py +++ b/hathor/builder/cli_builder.py @@ -28,7 +28,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 @@ -146,7 +145,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager: else: indexes = RocksDBIndexesManager(self.rocksdb_storage) - kwargs = {} + kwargs: dict[str, Any] = {} if not self._args.cache: # We should only pass indexes if cache is disabled. Otherwise, # only TransactionCacheStorage should have indexes. @@ -252,14 +251,8 @@ 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( - feature_settings=settings.FEATURE_ACTIVATION, - tx_storage=tx_storage - ) - bit_signaling_service = BitSignalingService( - feature_settings=settings.FEATURE_ACTIVATION, - feature_service=self.feature_service, + settings=settings, tx_storage=tx_storage, support_features=self._args.signal_support, not_support_features=self._args.signal_not_support, @@ -274,11 +267,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( verifiers=vertex_verifiers, tx_storage=tx_storage, @@ -310,7 +299,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, ) diff --git a/hathor/builder/resources_builder.py b/hathor/builder/resources_builder.py index 0e89448ab..d5330e10f 100644 --- a/hathor/builder/resources_builder.py +++ b/hathor/builder/resources_builder.py @@ -22,7 +22,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: @@ -39,7 +38,6 @@ def __init__( manager: 'HathorManager', args: 'RunNodeArgs', event_ws_factory: Optional['EventWebsocketFactory'], - feature_service: FeatureService ) -> None: self.log = logger.new() self.manager = manager @@ -50,8 +48,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() @@ -206,8 +202,7 @@ def create_resources(self) -> server.Site: ( b'feature', FeatureResource( - feature_settings=settings.FEATURE_ACTIVATION, - feature_service=self._feature_service, + settings=settings, tx_storage=self.manager.tx_storage ), root diff --git a/hathor/cli/mining.py b/hathor/cli/mining.py index d734d8e3b..f9a294d52 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(verifiers=verifiers, settings=settings) verification_service.verify_without_storage(block) except HathorError: diff --git a/hathor/cli/run_node.py b/hathor/cli/run_node.py index 052f4282c..10db00ba9 100644 --- a/hathor/cli/run_node.py +++ b/hathor/cli/run_node.py @@ -199,7 +199,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: @@ -221,7 +220,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 639eb1a5c..1bcbe1e53 100644 --- a/hathor/feature_activation/bit_signaling_service.py +++ b/hathor/feature_activation/bit_signaling_service.py @@ -14,11 +14,10 @@ from structlog import get_logger +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.settings import Settings as FeatureSettings from hathor.feature_activation.storage.feature_activation_storage import FeatureActivationStorage from hathor.transaction import Block from hathor.transaction.storage import TransactionStorage @@ -29,8 +28,7 @@ class BitSignalingService: __slots__ = ( '_log', - '_feature_settings', - '_feature_service', + '_settings', '_tx_storage', '_support_features', '_not_support_features', @@ -40,23 +38,20 @@ class BitSignalingService: def __init__( self, *, - feature_settings: FeatureSettings, - feature_service: FeatureService, + settings: HathorSettings, tx_storage: TransactionStorage, support_features: set[Feature], not_support_features: set[Feature], feature_storage: FeatureActivationStorage | None, ) -> None: self._log = logger.new() - self._feature_settings = feature_settings - self._feature_service = feature_service + self._settings = settings self._tx_storage = tx_storage 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: """ @@ -163,14 +158,14 @@ 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_descriptions = self._feature_service.get_bits_description(block=block) + feature_infos = block.static_metadata.get_feature_info(self._settings) signaling_features = { - feature: description.criteria - for feature, description in feature_descriptions.items() - if description.state in FeatureState.get_signaling_states() + feature: feature_info.criteria + for feature, feature_info in feature_infos.items() + if feature_info.state in FeatureState.get_signaling_states() } - assert len(signaling_features) <= self._feature_settings.max_signal_bits, ( + assert len(signaling_features) <= self._settings.FEATURE_ACTIVATION.max_signal_bits, ( 'Invalid state. Signaling more features than the allowed maximum.' ) diff --git a/hathor/feature_activation/feature_service.py b/hathor/feature_activation/feature_service.py index bc3003825..e0c7c5e5c 100644 --- a/hathor/feature_activation/feature_service.py +++ b/hathor/feature_activation/feature_service.py @@ -13,17 +13,18 @@ # limitations under the License. from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional, TypeAlias +from typing import TYPE_CHECKING, Callable, Optional, TypeAlias +from typing_extensions import Self + +from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature -from hathor.feature_activation.model.feature_description import FeatureDescription from hathor.feature_activation.model.feature_state import FeatureState -from hathor.feature_activation.settings import Settings as FeatureSettings +from hathor.transaction.storage import TransactionStorage +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,38 +43,48 @@ class BlockIsMissingSignal: class FeatureService: - __slots__ = ('_feature_settings', '_tx_storage', 'bit_signaling_service') - - def __init__(self, *, feature_settings: FeatureSettings, tx_storage: 'TransactionStorage') -> None: - self._feature_settings = feature_settings - self._tx_storage = tx_storage - self.bit_signaling_service: Optional['BitSignalingService'] = None + __slots__ = ('_feature_settings', '_vertex_getter', '_block_by_height_getter') - 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 + 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._vertex_getter = vertex_getter + self._block_by_height_getter = block_by_height_getter + + @classmethod + def create_from_storage(cls, settings: HathorSettings, storage: TransactionStorage) -> Self: + return cls( + settings=settings, + vertex_getter=storage.get_vertex, + block_by_height_getter=storage.get_block_by_height + ) - 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 - descriptions = self.get_bits_description(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_info(settings) must_signal_features = ( - feature for feature, description in descriptions.items() - if description.state is FeatureState.MUST_SIGNAL + feature for feature, feature_info in feature_infos.items() + if feature_info.state is FeatureState.MUST_SIGNAL ) 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 +93,50 @@ 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]: + 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 - # 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 # 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) + previous_boundary_block = self._get_ancestor_at_height( + block=block, + block_height=height, + ancestor_height=previous_boundary_height + ) + previous_boundary_state = previous_boundary_block.static_metadata.get_feature_state(feature) if offset_to_boundary != 0: return previous_boundary_state new_state = self._calculate_new_state( boundary_block=block, + height=height, feature=feature, previous_state=previous_boundary_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 +147,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 +155,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 +169,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) @@ -192,57 +202,42 @@ def _calculate_new_state( if previous_state is FeatureState.FAILED: return FeatureState.FAILED - raise ValueError(f'Unknown previous state: {previous_state}') - - def get_bits_description(self, *, block: 'Block') -> dict[Feature, FeatureDescription]: - """Returns the criteria definition and feature state for all features at a certain block.""" - return { - feature: FeatureDescription( - criteria=criteria, - state=self.get_state(block=block, feature=feature) - ) - for feature, criteria in self._feature_settings.features.items() - } + raise NotImplementedError(f'Unknown previous state: {previous_state}') - def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'Block': + def _get_ancestor_at_height(self, *, block: 'Block', block_height: int, 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. + Given a block, return its ancestor at a specific height using the best available method. """ - assert ancestor_height < block.static_metadata.height, ( + assert ancestor_height >= 0 + assert ancestor_height < block_height, ( f"ancestor height must be lower than the block's height: " - f"{ancestor_height} >= {block.static_metadata.height}" + f"{ancestor_height} >= {block_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 and (ancestor := self._tx_storage.get_block_by_height(ancestor_height)): - from hathor.transaction import Block - assert isinstance(ancestor, Block) - 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, ( + assert block_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() + ) # TODO: Check if it could be < instead of <= - return ancestor + from hathor.transaction import Block + ancestor = block + current_height = float('inf') + + while current_height > ancestor_height: + parent_block_hash = ancestor.get_block_parent_hash() + parent = self._vertex_getter(parent_block_hash) + assert isinstance(parent, Block) + ancestor = parent + current_height = ancestor.static_metadata.height + + if current_height == ancestor_height: + # We found the requested height, so we can return the ancestor. + return ancestor + + parent_meta = ancestor.get_metadata() + if parent_meta.validation.is_fully_connected() and not parent_meta.voided_by: + # We've reached a parent that is fully connected and confirmed, so it's guaranteed that the requested + # ancestor is in the height index. + ancestor_by_height = self._block_by_height_getter(ancestor_height) + assert ancestor_by_height is not None # TODO: move this guarantee to a separate PR. + return ancestor_by_height + + raise AssertionError('unreachable') diff --git a/hathor/feature_activation/model/feature_description.py b/hathor/feature_activation/model/feature_info.py similarity index 95% rename from hathor/feature_activation/model/feature_description.py rename to hathor/feature_activation/model/feature_info.py index a7f461c21..e2b8e7dda 100644 --- a/hathor/feature_activation/model/feature_description.py +++ b/hathor/feature_activation/model/feature_info.py @@ -18,7 +18,7 @@ from hathor.feature_activation.model.feature_state import FeatureState -class FeatureDescription(NamedTuple): +class FeatureInfo(NamedTuple): """Represents all information related to one feature, that is, its criteria and state.""" criteria: Criteria state: FeatureState diff --git a/hathor/feature_activation/model/feature_state.py b/hathor/feature_activation/model/feature_state.py index bb781f5eb..6020a9aa4 100644 --- a/hathor/feature_activation/model/feature_state.py +++ b/hathor/feature_activation/model/feature_state.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import Enum +from enum import Enum, unique -class FeatureState(Enum): +@unique +class FeatureState(str, Enum): """ Possible states a feature can be in, for each block. @@ -35,6 +36,10 @@ class FeatureState(Enum): ACTIVE = 'ACTIVE' FAILED = 'FAILED' + def is_active(self) -> bool: + """Return whether the state is active.""" + return self is FeatureState.ACTIVE + @staticmethod def get_signaling_states() -> set['FeatureState']: """ diff --git a/hathor/feature_activation/resources/feature.py b/hathor/feature_activation/resources/feature.py index f39fb1a37..59b5f4cd6 100644 --- a/hathor/feature_activation/resources/feature.py +++ b/hathor/feature_activation/resources/feature.py @@ -18,10 +18,9 @@ from hathor.api_util import Resource, set_cors 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.feature_activation.settings import Settings as FeatureSettings from hathor.transaction import Block from hathor.transaction.storage import TransactionStorage from hathor.utils.api import ErrorResponse, QueryParams, Response @@ -33,16 +32,10 @@ class FeatureResource(Resource): isLeaf = True - def __init__( - self, - *, - feature_settings: FeatureSettings, - feature_service: FeatureService, - tx_storage: TransactionStorage - ) -> None: + def __init__(self, *, settings: HathorSettings, tx_storage: TransactionStorage) -> None: super().__init__() - self._feature_settings = feature_settings - self._feature_service = feature_service + self._settings = settings + self._feature_settings = settings.FEATURE_ACTIVATION self.tx_storage = tx_storage def render_GET(self, request: Request) -> bytes: @@ -68,17 +61,17 @@ def get_block_features(self, request: Request) -> bytes: return error.json_dumpb() signal_bits = [] - feature_descriptions = self._feature_service.get_bits_description(block=block) + feature_infos = block.static_metadata.get_feature_info(self._settings) - for feature, description in feature_descriptions.items(): - if description.state not in FeatureState.get_signaling_states(): + for feature, feature_info in feature_infos.items(): + if feature_info.state not in FeatureState.get_signaling_states(): continue block_feature = GetBlockFeatureResponse( - bit=description.criteria.bit, - signal=block.get_feature_activation_bit_value(description.criteria.bit), + bit=feature_info.criteria.bit, + signal=block.get_feature_activation_bit_value(feature_info.criteria.bit), feature=feature, - feature_state=description.state.name + feature_state=feature_info.state.name ) signal_bits.append(block_feature) @@ -90,10 +83,12 @@ 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 = best_block.static_metadata.get_feature_info(self._settings) features = [] - for feature, criteria in self._feature_settings.features.items(): - state = self._feature_service.get_state(block=best_block, feature=feature) + for feature, feature_info in feature_infos.items(): + state = feature_info.state + criteria = feature_info.criteria threshold_count = criteria.get_threshold(self._feature_settings) threshold_percentage = threshold_count / self._feature_settings.evaluation_interval acceptance_percentage = None diff --git a/hathor/simulator/simulator.py b/hathor/simulator/simulator.py index 6155df3b8..06d8431c8 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_id import PeerId 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), daa=daa, - feature_service=feature_service, ) diff --git a/hathor/transaction/__init__.py b/hathor/transaction/__init__.py index 9b803cbd2..23e98d7ae 100644 --- a/hathor/transaction/__init__.py +++ b/hathor/transaction/__init__.py @@ -19,6 +19,7 @@ TxInput, TxOutput, TxVersion, + Vertex, sum_weights, ) from hathor.transaction.block import Block @@ -29,6 +30,7 @@ __all__ = [ 'Transaction', 'BitcoinAuxPow', + 'Vertex', 'BaseTransaction', 'Block', 'MergeMinedBlock', diff --git a/hathor/transaction/base_transaction.py b/hathor/transaction/base_transaction.py index 31e49d1db..1ca59eb0e 100644 --- a/hathor/transaction/base_transaction.py +++ b/hathor/transaction/base_transaction.py @@ -140,17 +140,20 @@ class GenericVertex(ABC, Generic[StaticMetadataT]): # bits reserved for future use, depending on the configuration. signal_bits: int - def __init__(self, - nonce: int = 0, - timestamp: Optional[int] = None, - signal_bits: int = 0, - version: TxVersion = TxVersion.REGULAR_BLOCK, - weight: float = 0, - inputs: Optional[list['TxInput']] = None, - outputs: Optional[list['TxOutput']] = None, - parents: Optional[list[VertexId]] = None, - hash: Optional[VertexId] = None, - storage: Optional['TransactionStorage'] = None) -> None: + def __init__( + self, + nonce: int = 0, + timestamp: Optional[int] = None, + signal_bits: int = 0, + version: TxVersion = TxVersion.REGULAR_BLOCK, + weight: float = 0, + inputs: Optional[list['TxInput']] = None, + outputs: Optional[list['TxOutput']] = None, + parents: Optional[list[VertexId]] = None, + hash: Optional[VertexId] = None, + storage: Optional['TransactionStorage'] = None, + settings: HathorSettings | None = None + ) -> None: """ Nonce: nonce used for the proof-of-work Timestamp: moment of creation @@ -163,7 +166,7 @@ def __init__(self, assert signal_bits <= _ONE_BYTE, f'signal_bits {hex(signal_bits)} must not be larger than one byte' assert version <= _ONE_BYTE, f'version {hex(version)} must not be larger than one byte' - self._settings = get_global_settings() + self._settings = settings or get_global_settings() self.nonce = nonce self.timestamp = timestamp or int(time.time()) self.signal_bits = signal_bits diff --git a/hathor/transaction/block.py b/hathor/transaction/block.py index 697de7ddd..700776788 100644 --- a/hathor/transaction/block.py +++ b/hathor/transaction/block.py @@ -20,8 +20,6 @@ from hathor.checkpoint import Checkpoint from hathor.conf.settings import HathorSettings -from hathor.feature_activation.feature import Feature -from hathor.feature_activation.model.feature_state import FeatureState from hathor.profiler import get_cpu_profiler from hathor.transaction import TxOutput, TxVersion from hathor.transaction.base_transaction import GenericVertex @@ -45,19 +43,32 @@ class Block(GenericVertex[BlockStaticMetadata]): SERIALIZATION_NONCE_SIZE = 16 - def __init__(self, - nonce: int = 0, - timestamp: Optional[int] = None, - signal_bits: int = 0, - version: TxVersion = TxVersion.REGULAR_BLOCK, - weight: float = 0, - outputs: Optional[list[TxOutput]] = None, - parents: Optional[list[bytes]] = None, - hash: Optional[bytes] = None, - data: bytes = b'', - storage: Optional['TransactionStorage'] = None) -> None: - super().__init__(nonce=nonce, timestamp=timestamp, signal_bits=signal_bits, version=version, weight=weight, - outputs=outputs or [], parents=parents or [], hash=hash, storage=storage) + def __init__( + self, + nonce: int = 0, + timestamp: Optional[int] = None, + signal_bits: int = 0, + version: TxVersion = TxVersion.REGULAR_BLOCK, + weight: float = 0, + outputs: Optional[list[TxOutput]] = None, + parents: Optional[list[bytes]] = None, + hash: Optional[bytes] = None, + data: bytes = b'', + storage: Optional['TransactionStorage'] = None, + settings: HathorSettings | None = None, + ) -> None: + super().__init__( + nonce=nonce, + timestamp=timestamp, + signal_bits=signal_bits, + version=version, + weight=weight, + outputs=outputs or [], + parents=parents or [], + hash=hash, + storage=storage, + settings=settings, + ) self.data = data def _get_formatted_fields_dict(self, short: bool = True) -> dict[str, str]: @@ -306,38 +317,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 b25081198..7fec480ff 100644 --- a/hathor/transaction/static_metadata.py +++ b/hathor/transaction/static_metadata.py @@ -12,28 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -import dataclasses from abc import ABC -from dataclasses import dataclass 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.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 -from hathor.util import json_dumpb, json_loadb +from hathor.util import json_loadb +from hathor.utils.pydantic import BaseModel if TYPE_CHECKING: from hathor.transaction import BaseTransaction, Block, Transaction from hathor.transaction.storage import TransactionStorage -@dataclass(slots=True, frozen=True, kw_only=True) -class VertexStaticMetadata(ABC): +class VertexStaticMetadata(ABC, BaseModel): """ Static Metadata represents vertex attributes that are not intrinsic to the vertex data, but can be calculated from only the vertex itself and its dependencies, and whose values never change. @@ -47,10 +46,6 @@ class VertexStaticMetadata(ABC): # metadata (that does not have this calculated, from a tx with a new format that does have this calculated) min_height: int - def to_bytes(self) -> bytes: - """Convert this static metadata instance to a json bytes representation.""" - return json_dumpb(dataclasses.asdict(self)) - @classmethod def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticMetadata': """Create a static metadata instance from a json bytes representation, with a known vertex type target.""" @@ -66,11 +61,10 @@ def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticM raise NotImplementedError -@dataclass(slots=True, frozen=True, kw_only=True) class BlockStaticMetadata(VertexStaticMetadata): height: int - # A list of feature activation bit counts. Must only be used by Blocks, is None otherwise. + # A list of feature activation bit counts. # Each list index corresponds to a bit position, and its respective value is the rolling count of active bits from # the previous boundary block up to this block, including it. LSB is on the left. feature_activation_bit_counts: list[int] @@ -81,18 +75,26 @@ 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`.""" + 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, @@ -104,7 +106,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 @@ -177,8 +179,23 @@ 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 self.feature_states.get(feature, FeatureState.DEFINED) + + def is_feature_active(self, feature: Feature) -> bool: + return self.get_feature_state(feature).is_active() + + def get_feature_info(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() + } + -@dataclass(slots=True, frozen=True, kw_only=True) class TransactionStaticMetadata(VertexStaticMetadata): @classmethod def create_from_storage(cls, tx: 'Transaction', settings: HathorSettings, storage: 'TransactionStorage') -> Self: diff --git a/hathor/transaction/storage/cache_storage.py b/hathor/transaction/storage/cache_storage.py index 08c966387..94d7e4a0f 100644 --- a/hathor/transaction/storage/cache_storage.py +++ b/hathor/transaction/storage/cache_storage.py @@ -18,6 +18,7 @@ from twisted.internet import threads from typing_extensions import override +from hathor.conf.settings import HathorSettings from hathor.indexes import IndexesManager from hathor.reactor import ReactorProtocol as Reactor from hathor.transaction import BaseTransaction @@ -35,8 +36,17 @@ class TransactionCacheStorage(BaseTransactionStorage): cache: OrderedDict[bytes, BaseTransaction] dirty_txs: set[bytes] - def __init__(self, store: 'BaseTransactionStorage', reactor: Reactor, interval: int = 5, - capacity: int = 10000, *, indexes: Optional[IndexesManager], _clone_if_needed: bool = False): + def __init__( + self, + store: 'BaseTransactionStorage', + reactor: Reactor, + interval: int = 5, + capacity: int = 10000, + *, + indexes: Optional[IndexesManager], + _clone_if_needed: bool = False, + settings: HathorSettings | None = None, + ) -> None: """ :param store: a subclass of BaseTransactionStorage :type store: :py:class:`hathor.transaction.storage.BaseTransactionStorage` @@ -71,7 +81,7 @@ def __init__(self, store: 'BaseTransactionStorage', reactor: Reactor, interval: # we need to use only one weakref dict, so we must first initialize super, and then # attribute the same weakref for both. - super().__init__(indexes=indexes) + super().__init__(indexes=indexes, settings=settings) self._tx_weakref = store._tx_weakref # XXX: just to make sure this isn't being used anywhere, setters/getters should be used instead del self._allow_scope diff --git a/hathor/transaction/storage/memory_storage.py b/hathor/transaction/storage/memory_storage.py index 26342b6ed..2030dfae0 100644 --- a/hathor/transaction/storage/memory_storage.py +++ b/hathor/transaction/storage/memory_storage.py @@ -16,6 +16,7 @@ from typing_extensions import override +from hathor.conf.settings import HathorSettings from hathor.indexes import IndexesManager from hathor.transaction import BaseTransaction from hathor.transaction.static_metadata import VertexStaticMetadata @@ -29,7 +30,13 @@ class TransactionMemoryStorage(BaseTransactionStorage): - def __init__(self, indexes: Optional[IndexesManager] = None, *, _clone_if_needed: bool = False) -> None: + def __init__( + self, + indexes: Optional[IndexesManager] = None, + *, + _clone_if_needed: bool = False, + settings: HathorSettings | None = None, + ) -> None: """ :param _clone_if_needed: *private parameter*, defaults to True, controls whether to clone transaction/blocks/metadata when returning those objects. @@ -41,7 +48,7 @@ def __init__(self, indexes: Optional[IndexesManager] = None, *, _clone_if_needed # Store custom key/value attributes self.attributes: dict[str, Any] = {} self._clone_if_needed = _clone_if_needed - super().__init__(indexes=indexes) + super().__init__(indexes=indexes, settings=settings) def _check_and_set_network(self) -> None: # XXX: does not apply to memory storage, can safely be ignored 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..5bcafb3b9 --- /dev/null +++ b/hathor/transaction/storage/migrations/migrate_feature_states.py @@ -0,0 +1,55 @@ +# 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 +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.""" + from hathor.feature_activation.feature_service import FeatureService + settings = get_global_settings() + log = logger.new() + topological_iterator = storage.topological_iterator() + feature_service = FeatureService( + settings=settings, + vertex_getter=storage.get_vertex, + block_by_height_getter=storage.get_block_by_height, + ) + + for vertex in progress(topological_iterator, log=log, total=None): + if not isinstance(vertex, Block): + continue + feature_states = feature_service.calculate_all_feature_states(vertex, height=vertex.static_metadata.height) + new_static_metadata = vertex.static_metadata.copy(update={'feature_states': feature_states}) + vertex._static_metadata = new_static_metadata + storage._save_static_metadata(vertex) diff --git a/hathor/transaction/storage/migrations/migrate_static_metadata.py b/hathor/transaction/storage/migrations/migrate_static_metadata.py index d7f7721d9..c89c7f393 100644 --- a/hathor/transaction/storage/migrations/migrate_static_metadata.py +++ b/hathor/transaction/storage/migrations/migrate_static_metadata.py @@ -65,7 +65,7 @@ def run(self, storage: 'TransactionStorage') -> 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/migrations/remove_first_nop_features.py b/hathor/transaction/storage/migrations/remove_first_nop_features.py index 555bcf741..49c16b577 100644 --- a/hathor/transaction/storage/migrations/remove_first_nop_features.py +++ b/hathor/transaction/storage/migrations/remove_first_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,22 +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 vertex.is_block: - meta = vertex.get_metadata() - assert meta.height is not None - # This is the start_height of the **second** Phased Testing, so we clear anything before it. - if meta.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 6361448a2..68e5d6ccd 100644 --- a/hathor/transaction/storage/rocksdb_storage.py +++ b/hathor/transaction/storage/rocksdb_storage.py @@ -17,6 +17,7 @@ from structlog import get_logger from typing_extensions import override +from hathor.conf.settings import HathorSettings from hathor.indexes import IndexesManager from hathor.storage import RocksDBStorage from hathor.transaction.static_metadata import VertexStaticMetadata @@ -47,7 +48,12 @@ class TransactionRocksDBStorage(BaseTransactionStorage): It uses Protobuf serialization internally. """ - def __init__(self, rocksdb_storage: RocksDBStorage, indexes: Optional[IndexesManager] = None): + def __init__( + self, + rocksdb_storage: RocksDBStorage, + indexes: Optional[IndexesManager] = None, + settings: HathorSettings | None = None + ) -> None: self._cf_tx = rocksdb_storage.get_or_create_column_family(_CF_NAME_TX) self._cf_meta = rocksdb_storage.get_or_create_column_family(_CF_NAME_META) self._cf_static_meta = rocksdb_storage.get_or_create_column_family(_CF_NAME_STATIC_META) @@ -56,7 +62,7 @@ def __init__(self, rocksdb_storage: RocksDBStorage, indexes: Optional[IndexesMan self._rocksdb_storage = rocksdb_storage self._db = rocksdb_storage.get_db() - super().__init__(indexes=indexes) + super().__init__(indexes=indexes, settings=settings) def _load_from_bytes(self, tx_data: bytes, meta_data: bytes) -> 'BaseTransaction': from hathor.transaction.base_transaction import tx_or_block_from_bytes @@ -107,7 +113,7 @@ def _save_transaction(self, tx: 'BaseTransaction', *, only_metadata: bool = Fals @override def _save_static_metadata(self, tx: 'BaseTransaction') -> None: - self._db.put((self._cf_static_meta, tx.hash), tx.static_metadata.to_bytes()) + self._db.put((self._cf_static_meta, tx.hash), tx.static_metadata.json_dumpb()) @override def _get_static_metadata(self, vertex: 'BaseTransaction') -> VertexStaticMetadata | None: diff --git a/hathor/transaction/storage/transaction_storage.py b/hathor/transaction/storage/transaction_storage.py index 76ab9075c..deeb5ef65 100644 --- a/hathor/transaction/storage/transaction_storage.py +++ b/hathor/transaction/storage/transaction_storage.py @@ -24,6 +24,7 @@ from structlog import get_logger from hathor.conf.get_settings import get_global_settings +from hathor.conf.settings import HathorSettings from hathor.execution_manager import ExecutionManager from hathor.indexes import IndexesManager from hathor.indexes.height_index import HeightInfo @@ -44,6 +45,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,12 +105,13 @@ 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] - def __init__(self) -> None: - self._settings = get_global_settings() + def __init__(self, settings: HathorSettings | None = None) -> None: + self._settings = settings or get_global_settings() # Weakref is used to guarantee that there is only one instance of each transaction in memory. self._tx_weakref: WeakValueDictionary[bytes, BaseTransaction] = WeakValueDictionary() self._tx_weakref_disabled: bool = False @@ -1129,6 +1132,7 @@ def _construct_genesis_block(self) -> Block: """Return the genesis block.""" block = Block( storage=self, + settings=self._settings, nonce=self._settings.GENESIS_BLOCK_NONCE, timestamp=self._settings.GENESIS_BLOCK_TIMESTAMP, weight=self._settings.MIN_BLOCK_WEIGHT, @@ -1190,8 +1194,13 @@ def iter_all_raw_metadata(self) -> Iterator[tuple[VertexId, dict[str, Any]]]: class BaseTransactionStorage(TransactionStorage): indexes: Optional[IndexesManager] - def __init__(self, indexes: Optional[IndexesManager] = None, pubsub: Optional[Any] = None) -> None: - super().__init__() + def __init__( + self, + indexes: Optional[IndexesManager] = None, + pubsub: Optional[Any] = None, + settings: HathorSettings | None = None, + ) -> None: + super().__init__(settings=settings) # Pubsub is used to publish tx voided and winner but it's optional self.pubsub = pubsub diff --git a/hathor/transaction/transaction_metadata.py b/hathor/transaction/transaction_metadata.py index 03daacf21..39c941ef6 100644 --- a/hathor/transaction/transaction_metadata.py +++ b/hathor/transaction/transaction_metadata.py @@ -16,8 +16,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.static_metadata import BlockStaticMetadata from hathor.transaction.validation_state import ValidationState from hathor.util import practically_equal @@ -43,10 +41,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]'] @@ -171,8 +165,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 @@ -208,12 +201,12 @@ def to_json(self) -> dict[str, Any]: data['min_height'] = self.min_height from hathor.transaction import Block - if isinstance(self.get_tx(), Block): + vertex = self.get_tx() + if isinstance(vertex, Block): + feature_states = vertex.static_metadata.feature_states data['height'] = self.height - data['feature_activation_bit_counts'] = self.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()} + 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()} if self.first_block is not None: data['first_block'] = self.first_block.hex() @@ -261,13 +254,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) @@ -329,13 +315,3 @@ def min_height(self) -> int: backwards compatibility. It can be removed in the future. """ return self.get_tx().static_metadata.min_height - - @property - def feature_activation_bit_counts(self) -> list[int]: - """ - Get the block's `feature_activation_bit_counts`. This property is just a forward from the block's - `static_metadata`, for backwards compatibility. It can be removed in the future. - """ - static_metadata = self.get_tx().static_metadata - assert isinstance(static_metadata, BlockStaticMetadata) - return static_metadata.feature_activation_bit_counts diff --git a/hathor/verification/block_verifier.py b/hathor/verification/block_verifier.py index ff0c74a86..265c06e44 100644 --- a/hathor/verification/block_verifier.py +++ b/hathor/verification/block_verifier.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing_extensions import assert_never + from hathor.conf.settings import HathorSettings from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.feature_activation.feature_service import BlockIsMissingSignal, BlockIsSignaling, FeatureService @@ -30,18 +32,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.""" @@ -88,7 +83,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(): @@ -98,5 +93,4 @@ def verify_mandatory_signaling(self, block: Block) -> None: f"Block must signal support for feature '{feature.value}' during MUST_SIGNAL phase." ) case _: - # TODO: This will be changed to assert_never() so mypy can check it. - raise NotImplementedError + assert_never(signaling_state) 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/verification_service.py b/hathor/verification/verification_service.py index ac70f9979..12f06a203 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -178,6 +178,7 @@ def _verify_block(self, block: Block) -> None: self.verifiers.block.verify_mandatory_signaling(block) def _verify_merge_mined_block(self, block: MergeMinedBlock) -> None: + self.verifiers.merge_mined_block.verify_aux_pow(block) self._verify_block(block) @cpu.profiler(key=lambda _, tx: 'tx-verify!{}'.format(tx.hash.hex())) @@ -249,7 +250,6 @@ def _verify_without_storage_block(self, block: Block) -> None: self.verifiers.vertex.verify_sigops_output(block) def _verify_without_storage_merge_mined_block(self, block: MergeMinedBlock) -> None: - self.verifiers.merge_mined_block.verify_aux_pow(block) self._verify_without_storage_block(block) def _verify_without_storage_tx(self, tx: Transaction) -> None: diff --git a/hathor/verification/vertex_verifiers.py b/hathor/verification/vertex_verifiers.py index 98477c397..7d865ff53 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.token_creation_transaction_verifier import TokenCreationTransactionVerifier @@ -33,13 +32,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. @@ -50,7 +43,6 @@ def create_defaults( settings=settings, vertex_verifier=vertex_verifier, daa=daa, - feature_service=feature_service ) @classmethod @@ -60,13 +52,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) 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 862677f84..047dcb6b4 100644 --- a/hathor/vertex_handler/vertex_handler.py +++ b/hathor/vertex_handler/vertex_handler.py @@ -19,8 +19,6 @@ from hathor.conf.settings import HathorSettings from hathor.consensus import ConsensusAlgorithm from hathor.exception import HathorError, InvalidNewTransaction -from hathor.feature_activation.feature import Feature -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 @@ -42,7 +40,6 @@ class VertexHandler: '_verification_service', '_consensus', '_p2p_manager', - '_feature_service', '_pubsub', '_wallet', ) @@ -56,7 +53,6 @@ def __init__( verification_service: VerificationService, consensus: ConsensusAlgorithm, p2p_manager: ConnectionsManager, - feature_service: FeatureService, pubsub: PubSubManager, wallet: BaseWallet | None, ) -> None: @@ -67,7 +63,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 @@ -203,6 +198,12 @@ def _post_consensus( # TODO Remove it and use pubsub instead. self._wallet.on_new_tx(vertex) + # TODO: if new feature state is must signal, call bit_signaling_service.on_must_signal(). + # In fact, create a new `bit_signaling_service.on_new_vertex` method that does this logic + # and use NETWORK_NEW_TX_ACCEPTED there + # test + # service.bit_signaling_service.on_must_signal.assert_called_once_with(Feature.NOP_FEATURE_1) + self._log_new_object(vertex, 'new {}', quiet=quiet) self._log_feature_states(vertex) @@ -238,10 +239,10 @@ def _log_feature_states(self, vertex: BaseTransaction) -> None: if not isinstance(vertex, Block): return - feature_descriptions = self._feature_service.get_bits_description(block=vertex) + feature_infos = vertex.static_metadata.get_feature_info(self._settings) state_by_feature = { - feature.value: description.state.value - for feature, description in feature_descriptions.items() + feature.value: feature_info.state.value + for feature, feature_info in feature_infos.items() } self._log.info( @@ -250,17 +251,19 @@ def _log_feature_states(self, vertex: BaseTransaction) -> None: block_height=vertex.get_height(), features_states=state_by_feature ) + self._log_active_features(vertex) + + def _log_active_features(self, block: Block) -> None: + """Log ACTIVE features for a block. Used as part of the Feature Activation Phased Testing.""" + if not self._settings.NETWORK_NAME == 'mainnet': + # We're currently only performing phased testing on mainnet, so we won't log in other networks. + return - features = [Feature.NOP_FEATURE_1, Feature.NOP_FEATURE_2] - for feature in features: - self._log_if_feature_is_active(vertex, feature) - - def _log_if_feature_is_active(self, block: Block, feature: Feature) -> None: - """Log if a feature is ACTIVE for a block. Used as part of the Feature Activation Phased Testing.""" - if self._feature_service.is_feature_active(block=block, feature=feature): - self._log.info( - 'Feature is ACTIVE for block', - feature=feature.value, - block_hash=block.hash_hex, - block_height=block.get_height() - ) + for feature in self._settings.FEATURE_ACTIVATION.features: + if block.static_metadata.is_feature_active(feature): + self._log.info( + 'Feature is ACTIVE for block', + feature=feature.value, + block_hash=block.hash_hex, + block_height=block.get_height() + ) diff --git a/tests/feature_activation/test_bit_signaling_service.py b/tests/feature_activation/test_bit_signaling_service.py index 930ca39f2..f71ae6c86 100644 --- a/tests/feature_activation/test_bit_signaling_service.py +++ b/tests/feature_activation/test_bit_signaling_service.py @@ -16,14 +16,14 @@ import pytest +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_description import FeatureDescription +from hathor.feature_activation.model.feature_info import FeatureInfo from hathor.feature_activation.model.feature_state import FeatureState -from hathor.feature_activation.settings import Settings as FeatureSettings from hathor.transaction import Block +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.storage import TransactionStorage @@ -32,11 +32,11 @@ [ {}, { - Feature.NOP_FEATURE_1: FeatureDescription(state=FeatureState.DEFINED, criteria=Mock()) + Feature.NOP_FEATURE_1: FeatureInfo(state=FeatureState.DEFINED, criteria=Mock()) }, { - Feature.NOP_FEATURE_1: FeatureDescription(state=FeatureState.FAILED, criteria=Mock()), - Feature.NOP_FEATURE_2: FeatureDescription(state=FeatureState.ACTIVE, criteria=Mock()) + Feature.NOP_FEATURE_1: FeatureInfo(state=FeatureState.FAILED, criteria=Mock()), + Feature.NOP_FEATURE_2: FeatureInfo(state=FeatureState.ACTIVE, criteria=Mock()) } ] ) @@ -50,7 +50,7 @@ ] ) def test_generate_signal_bits_no_signaling_features( - features_description: dict[Feature, FeatureDescription], + features_description: dict[Feature, FeatureInfo], support_features: set[Feature], not_support_features: set[Feature] ) -> None: @@ -74,7 +74,7 @@ def test_generate_signal_bits_signaling_features( expected_signal_bits: int, ) -> None: features_description = { - Feature.NOP_FEATURE_1: FeatureDescription( + Feature.NOP_FEATURE_1: FeatureInfo( state=FeatureState.STARTED, criteria=Criteria( bit=0, @@ -83,7 +83,7 @@ def test_generate_signal_bits_signaling_features( version='0.0.0' ) ), - Feature.NOP_FEATURE_2: FeatureDescription( + Feature.NOP_FEATURE_2: FeatureInfo( state=FeatureState.MUST_SIGNAL, criteria=Criteria( bit=1, @@ -92,7 +92,7 @@ def test_generate_signal_bits_signaling_features( version='0.0.0' ) ), - Feature.NOP_FEATURE_3: FeatureDescription( + Feature.NOP_FEATURE_3: FeatureInfo( state=FeatureState.LOCKED_IN, criteria=Criteria( bit=3, @@ -123,8 +123,8 @@ def test_generate_signal_bits_signaling_features_with_defaults( not_support_features: set[Feature], expected_signal_bits: int, ) -> None: - features_description = { - Feature.NOP_FEATURE_1: FeatureDescription( + feature_infos = { + Feature.NOP_FEATURE_1: FeatureInfo( state=FeatureState.STARTED, criteria=Criteria( bit=0, @@ -134,7 +134,7 @@ def test_generate_signal_bits_signaling_features_with_defaults( signal_support_by_default=True ) ), - Feature.NOP_FEATURE_2: FeatureDescription( + Feature.NOP_FEATURE_2: FeatureInfo( state=FeatureState.MUST_SIGNAL, criteria=Criteria( bit=1, @@ -144,7 +144,7 @@ def test_generate_signal_bits_signaling_features_with_defaults( signal_support_by_default=True ) ), - Feature.NOP_FEATURE_3: FeatureDescription( + Feature.NOP_FEATURE_3: FeatureInfo( state=FeatureState.LOCKED_IN, criteria=Criteria( bit=3, @@ -155,29 +155,31 @@ def test_generate_signal_bits_signaling_features_with_defaults( ) } - signal_bits = _test_generate_signal_bits(features_description, support_features, not_support_features) + signal_bits = _test_generate_signal_bits(feature_infos, support_features, not_support_features) assert signal_bits == expected_signal_bits def _test_generate_signal_bits( - features_description: dict[Feature, FeatureDescription], + feature_infos: dict[Feature, FeatureInfo], support_features: set[Feature], not_support_features: set[Feature] ) -> int: - feature_service = Mock(spec_set=FeatureService) - feature_service.get_bits_description = lambda block: features_description + settings = Mock(spec_set=HathorSettings) + settings.FEATURE_ACTIVATION.max_signal_bits = 4 + + block = Mock(spec_set=Block) + block.static_metadata.get_feature_info = lambda _: feature_infos service = BitSignalingService( - feature_settings=FeatureSettings(), - feature_service=feature_service, + settings=settings, tx_storage=Mock(), support_features=support_features, not_support_features=not_support_features, feature_storage=Mock(), ) - return service.generate_signal_bits(block=Mock()) + return service.generate_signal_bits(block=block) @pytest.mark.parametrize( @@ -212,8 +214,7 @@ def test_support_intersection_validation( ) -> None: with pytest.raises(ValueError) as e: BitSignalingService( - feature_settings=Mock(), - feature_service=Mock(), + settings=Mock(), tx_storage=Mock(), support_features=support_features, not_support_features=not_support_features, @@ -252,23 +253,23 @@ def test_non_signaling_features_warning( not_support_features: set[Feature], non_signaling_features: set[str], ) -> None: - best_block = Mock(spec_set=Block) - best_block.get_height = Mock(return_value=123) - best_block.hash_hex = 'abc' + settings = Mock(spec_set=HathorSettings) + settings.FEATURE_ACTIVATION.features = {} + settings.FEATURE_ACTIVATION.max_signal_bits = 4 + + 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_bits_description_mock(block: Block) -> dict[Feature, FeatureDescription]: - if block == best_block: - return {} - raise NotImplementedError - - feature_service = Mock(spec_set=FeatureService) - feature_service.get_bits_description = get_bits_description_mock - service = BitSignalingService( - feature_settings=FeatureSettings(), - feature_service=feature_service, + settings=settings, tx_storage=tx_storage, support_features=support_features, not_support_features=not_support_features, @@ -283,15 +284,14 @@ def get_bits_description_mock(block: Block) -> dict[Feature, FeatureDescription] '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: service = BitSignalingService( - feature_settings=Mock(), - feature_service=Mock(), + settings=Mock(), tx_storage=Mock(), support_features=set(), not_support_features={Feature.NOP_FEATURE_1}, @@ -306,8 +306,7 @@ def test_on_must_signal_not_supported() -> None: def test_on_must_signal_supported() -> None: service = BitSignalingService( - feature_settings=Mock(), - feature_service=Mock(), + settings=Mock(), tx_storage=Mock(), support_features=set(), not_support_features=set(), diff --git a/tests/feature_activation/test_feature_service.py b/tests/feature_activation/test_feature_service.py index 9695340b1..3036bc5a3 100644 --- a/tests/feature_activation/test_feature_service.py +++ b/tests/feature_activation/test_feature_service.py @@ -17,6 +17,7 @@ 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, @@ -25,20 +26,20 @@ FeatureService, ) from hathor.feature_activation.model.criteria import Criteria -from hathor.feature_activation.model.feature_description import FeatureDescription +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.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 -@pytest.fixture -def storage() -> TransactionStorage: +def get_storage(settings: HathorSettings, *, up_to_height: int) -> TransactionStorage: indexes = MemoryIndexesManager() - storage = TransactionMemoryStorage(indexes=indexes) + storage = TransactionMemoryStorage(indexes=indexes, settings=settings) feature_activation_bits = [ 0b0000, # 0: boundary block 0b0010, @@ -74,50 +75,49 @@ def storage() -> TransactionStorage: 0b0000, ] - for height, bits in enumerate(feature_activation_bits): + for height, bits in enumerate(feature_activation_bits[:up_to_height + 1]): if height == 0: continue parent = not_none(storage.get_block_by_height(height - 1)) block = Block(signal_bits=bits, parents=[parent.hash], storage=storage) block.update_hash() block.get_metadata().validation = ValidationState.FULL - block.init_static_metadata_from_storage(get_global_settings(), storage) + block.init_static_metadata_from_storage(settings, storage) storage.save_transaction(block) indexes.height.add_new(height, block.hash, block.timestamp) return storage -@pytest.fixture -def feature_settings() -> FeatureSettings: - return FeatureSettings( +def get_settings(*, features: dict[Feature, Criteria]) -> HathorSettings: + feature_settings = FeatureSettings.construct( evaluation_interval=4, - default_threshold=3 - ) - - -@pytest.fixture -def service(feature_settings: FeatureSettings, storage: TransactionStorage) -> FeatureService: - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage + default_threshold=3, + features=features, ) - service.bit_signaling_service = Mock() + settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) + return settings - return service - -def test_get_state_genesis(storage: TransactionStorage, service: FeatureService) -> None: +def test_calculate_state_genesis() -> None: + settings = get_settings(features={}) + storage = get_storage(settings, up_to_height=0) + service = FeatureService.create_from_storage(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(storage: TransactionStorage, service: FeatureService, 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.create_from_storage(settings, storage) block = not_none(storage.get_block_by_height(block_height)) - result = service.get_state(block=block, feature=Mock()) + result = service._calculate_state(block=block, height=block_height, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.DEFINED @@ -131,45 +131,32 @@ def test_get_state_first_interval(storage: TransactionStorage, service: FeatureS (8, FeatureState.DEFINED) ] ) -def test_get_state_from_defined( - storage: TransactionStorage, - block_height: int, - start_height: int, - expected_state: FeatureState -) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ - Feature.NOP_FEATURE_1: Criteria.construct( - bit=Mock(), - start_height=start_height, - timeout_height=Mock(), - version=Mock() - ) - } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() +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(), + start_height=start_height, + timeout_height=Mock(), + version=Mock() + ) + } + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_started_to_failed( block_height: int, timeout_height: int, ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -178,29 +165,23 @@ def test_get_state_from_started_to_failed( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_started_to_must_signal_on_timeout( block_height: int, timeout_height: int, ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -209,24 +190,19 @@ def test_get_state_from_started_to_must_signal_on_timeout( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_started_to_locked_in_on_default_threshold( block_height: int, default_threshold: int ) -> None: @@ -243,28 +219,23 @@ def test_get_state_from_started_to_locked_in_on_default_threshold( ) } ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_started_to_locked_in_on_custom_threshold( block_height: int, custom_threshold: int ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=1, start_height=0, @@ -273,15 +244,12 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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 @@ -295,15 +263,12 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( (True, 20), ] ) -def test_get_state_from_started_to_started( - storage: TransactionStorage, +def test_calculate_state_from_started_to_started( block_height: int, lock_in_on_timeout: bool, timeout_height: int, ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -312,27 +277,21 @@ def test_get_state_from_started_to_started( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_must_signal_to_locked_in( block_height: int, ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -341,29 +300,23 @@ def test_get_state_from_must_signal_to_locked_in( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_locked_in_to_active( block_height: int, minimum_activation_height: int, ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -373,29 +326,23 @@ def test_get_state_from_locked_in_to_active( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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( - storage: TransactionStorage, +def test_calculate_state_from_locked_in_to_locked_in( block_height: int, minimum_activation_height: int, ) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -405,24 +352,19 @@ def test_get_state_from_locked_in_to_locked_in( version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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(storage: TransactionStorage, block_height: int) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ +def test_calculate_state_from_active(block_height: int) -> None: + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -431,24 +373,19 @@ def test_get_state_from_active(storage: TransactionStorage, block_height: int) - version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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]) -def test_caching_mechanism(storage: TransactionStorage, block_height: int) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ +def test_is_feature_active(block_height: int) -> None: + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=3, start_height=0, @@ -457,56 +394,18 @@ def test_caching_mechanism(storage: TransactionStorage, block_height: int) -> No version=Mock() ) } - ) - service = FeatureService(feature_settings=feature_settings, tx_storage=storage) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) block = not_none(storage.get_block_by_height(block_height)) - calculate_new_state_mock = Mock(wraps=service._calculate_new_state) - - with patch.object(FeatureService, '_calculate_new_state', calculate_new_state_mock): - result1 = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) - - assert result1 == FeatureState.ACTIVE - assert calculate_new_state_mock.call_count == 4 - - calculate_new_state_mock.reset_mock() - result2 = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) - - assert result2 == FeatureState.ACTIVE - assert calculate_new_state_mock.call_count == 0 - -@pytest.mark.parametrize('block_height', [16, 17, 18, 19]) -def test_is_feature_active(storage: TransactionStorage, block_height: int) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ - Feature.NOP_FEATURE_1: Criteria.construct( - bit=3, - start_height=0, - timeout_height=8, - lock_in_on_timeout=True, - version=Mock() - ) - } - ) - service = FeatureService( - feature_settings=feature_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(storage: TransactionStorage, block_height: int) -> None: - feature_settings = FeatureSettings.construct( - evaluation_interval=4, - features={ +def test_calculate_state_from_failed(block_height: int) -> None: + features = { Feature.NOP_FEATURE_1: Criteria.construct( bit=Mock(), start_height=0, @@ -514,54 +413,49 @@ def test_get_state_from_failed(storage: TransactionStorage, block_height: int) - version=Mock() ) } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() + settings = get_settings(features=features) + storage = get_storage(settings, up_to_height=block_height) + service = FeatureService.create_from_storage(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(storage: TransactionStorage, service: FeatureService) -> 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)) - result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) + service = FeatureService.create_from_storage(settings, storage) + result = service._calculate_state(block=block, height=10, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.DEFINED -def test_get_bits_description(storage: TransactionStorage) -> 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()) - feature_settings = FeatureSettings.construct( - features={ - Feature.NOP_FEATURE_1: criteria_mock_1, - Feature.NOP_FEATURE_2: criteria_mock_2 - } - ) - service = FeatureService( - feature_settings=feature_settings, - tx_storage=storage - ) - service.bit_signaling_service = Mock() - - def get_state(self: FeatureService, *, block: Block, feature: Feature) -> FeatureState: - states = { +def test_get_feature_info() -> None: + 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 + }) + 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_bits_description(block=Mock()) + ) + block.set_static_metadata(static_metadata) + result = block.static_metadata.get_feature_info(settings) expected = { - Feature.NOP_FEATURE_1: FeatureDescription(criteria_mock_1, FeatureState.STARTED), - Feature.NOP_FEATURE_2: FeatureDescription(criteria_mock_2, FeatureState.FAILED), + Feature.NOP_FEATURE_1: FeatureInfo(criteria_mock_1, FeatureState.STARTED), + Feature.NOP_FEATURE_2: FeatureInfo(criteria_mock_2, FeatureState.FAILED), } assert result == expected @@ -577,18 +471,14 @@ def get_state(self: FeatureService, *, block: Block, feature: Feature) -> Featur (0, 0), ] ) -def test_get_ancestor_at_height_invalid( - feature_settings: FeatureSettings, - storage: TransactionStorage, - block_height: int, - ancestor_height: int -) -> None: - service = FeatureService(feature_settings=feature_settings, tx_storage=storage) - service.bit_signaling_service = Mock() +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.create_from_storage(settings, storage) 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) + service._get_ancestor_at_height(block=block, block_height=block_height, ancestor_height=ancestor_height) assert str(e.value) == ( f"ancestor height must be lower than the block's height: {ancestor_height} >= {block_height}" @@ -599,26 +489,26 @@ def test_get_ancestor_at_height_invalid( ['block_height', 'ancestor_height'], [ (21, 20), - (21, 10), - (21, 0), - (15, 10), - (15, 0), + (21, 18), + (21, 17), + (15, 12), + (15, 11), (1, 0), ] ) -def test_get_ancestor_at_height( - feature_settings: FeatureSettings, - storage: TransactionStorage, - block_height: int, - ancestor_height: int -) -> None: - service = FeatureService(feature_settings=feature_settings, tx_storage=storage) - service.bit_signaling_service = Mock() +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): - result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) + service = FeatureService.create_from_storage(settings, storage) + result = service._get_ancestor_at_height( + block=block, + block_height=block_height, + ancestor_height=ancestor_height + ) assert get_block_by_height_wrapped.call_count == ( 0 if block_height - ancestor_height <= 1 else 1 @@ -633,25 +523,26 @@ def test_get_ancestor_at_height( (21, 20), (21, 18), (15, 12), - (15, 10), + (15, 11), (1, 0), ] ) -def test_get_ancestor_at_height_voided( - feature_settings: FeatureSettings, - storage: TransactionStorage, - block_height: int, - ancestor_height: int -) -> None: - service = FeatureService(feature_settings=feature_settings, tx_storage=storage) - service.bit_signaling_service = Mock() +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.create_from_storage(settings, storage) 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) + result = service._get_ancestor_at_height( + block=block, + block_height=block_height, + ancestor_height=ancestor_height + ) assert get_block_by_height_wrapped.call_count == 0 assert result == storage.get_block_by_height(ancestor_height) @@ -685,7 +576,6 @@ def test_get_ancestor_at_height_voided( ] ) def test_check_must_signal( - storage: TransactionStorage, bit: int, threshold: int, block_height: int, @@ -704,10 +594,10 @@ def test_check_must_signal( ) } ) - service = FeatureService(feature_settings=feature_settings, tx_storage=storage) - service.bit_signaling_service = Mock() + settings = get_global_settings()._replace(FEATURE_ACTIVATION=feature_settings) + storage = get_storage(settings, up_to_height=block_height) 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 2c5e9094d..ad927aeeb 100644 --- a/tests/feature_activation/test_feature_simulation.py +++ b/tests/feature_activation/test_feature_simulation.py @@ -13,14 +13,13 @@ # limitations under the License. from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import Mock import pytest from hathor.builder import Builder from hathor.conf.get_settings import get_global_settings 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.resources.feature import FeatureResource from hathor.feature_activation.settings import Settings as FeatureSettings @@ -56,8 +55,7 @@ def _calculate_new_state_mock_block_height_calls(calculate_new_state_mock: Mock) def test_feature(self) -> None: """ - Tests that a feature goes through all possible states in the correct block heights, and also assert internal - method calls to make sure we're executing it in the intended, most performatic way. + Tests that a feature goes through all possible states in the correct block heights. """ feature_settings = FeatureSettings( evaluation_interval=4, @@ -78,262 +76,215 @@ 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( - feature_settings=feature_settings, - feature_service=feature_service, + settings=settings, tx_storage=artifacts.tx_storage ) web_client = StubSite(feature_resource) - calculate_new_state_mock = Mock(wraps=feature_service._calculate_new_state) - get_ancestor_iteratively_mock = Mock(wraps=feature_service._get_ancestor_iteratively) - - with ( - patch.object(FeatureService, '_calculate_new_state', calculate_new_state_mock), - patch.object(FeatureService, '_get_ancestor_iteratively', get_ancestor_iteratively_mock), - ): - # at the beginning, the feature is DEFINED: - add_new_blocks(manager, 10) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=10, - features=[ - dict( - name='NOP_FEATURE_1', - state='DEFINED', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - # so we calculate states all the way down to the first evaluation boundary (after genesis): - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 4 - # no blocks are voided, so we only use the height index, and not get_ancestor_iteratively: - assert get_ancestor_iteratively_mock.call_count == 0 - calculate_new_state_mock.reset_mock() - - # at block 19, the feature is DEFINED, just before becoming STARTED: - add_new_blocks(manager, 9) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=19, - features=[ - dict( - name='NOP_FEATURE_1', - state='DEFINED', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - # so we calculate states down to block 12, as block 8's state is saved: - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 12 - assert get_ancestor_iteratively_mock.call_count == 0 - calculate_new_state_mock.reset_mock() - - # at block 20, the feature becomes STARTED: - add_new_blocks(manager, 1) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=20, - features=[ - dict( - name='NOP_FEATURE_1', - state='STARTED', - acceptance=0, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 20 - assert get_ancestor_iteratively_mock.call_count == 0 - - # we add one block before resetting the mock, just to make sure block 20 gets a chance to be saved - add_new_blocks(manager, 1) - calculate_new_state_mock.reset_mock() - - # at block 55, the feature is STARTED, just before becoming MUST_SIGNAL: - add_new_blocks(manager, 34) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=55, - features=[ - dict( - name='NOP_FEATURE_1', - state='STARTED', - acceptance=0, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 24 - assert get_ancestor_iteratively_mock.call_count == 0 - calculate_new_state_mock.reset_mock() - - # at block 56, the feature becomes MUST_SIGNAL: - add_new_blocks(manager, 1) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=56, - features=[ - dict( - name='NOP_FEATURE_1', - state='MUST_SIGNAL', - acceptance=0, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 56 - assert get_ancestor_iteratively_mock.call_count == 0 - - # we add one block before resetting the mock, just to make sure block 56 gets a chance to be saved - add_new_blocks(manager, 1, signal_bits=0b1) - calculate_new_state_mock.reset_mock() - - # if we try to propagate a non-signaling block, it is not accepted - non_signaling_block = manager.generate_mining_block() - manager.cpu_mining_service.resolve(non_signaling_block) - non_signaling_block.signal_bits = 0b10 - non_signaling_block.init_static_metadata_from_storage(settings, manager.tx_storage) - - with pytest.raises(BlockMustSignalError): - manager.verification_service.verify(non_signaling_block) - - assert not manager.propagate_tx(non_signaling_block) - - # at block 59, the feature is MUST_SIGNAL, just before becoming LOCKED_IN: - add_new_blocks(manager, num_blocks=2, signal_bits=0b1) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=59, - features=[ - dict( - name='NOP_FEATURE_1', - state='MUST_SIGNAL', - acceptance=0.75, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - # we don't need to calculate any new state, as block 56's state is saved: - assert len(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 0 - assert get_ancestor_iteratively_mock.call_count == 0 - calculate_new_state_mock.reset_mock() - - # at block 60, the feature becomes LOCKED_IN: - add_new_blocks(manager, 1) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=60, - features=[ - dict( - name='NOP_FEATURE_1', - state='LOCKED_IN', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 60 - assert get_ancestor_iteratively_mock.call_count == 0 - - # we add one block before resetting the mock, just to make sure block 60 gets a chance to be saved - add_new_blocks(manager, 1) - calculate_new_state_mock.reset_mock() - - # at block 71, the feature is LOCKED_IN, just before becoming ACTIVE: - add_new_blocks(manager, 10) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=71, - features=[ - dict( - name='NOP_FEATURE_1', - state='LOCKED_IN', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 64 - assert get_ancestor_iteratively_mock.call_count == 0 - calculate_new_state_mock.reset_mock() - - # at block 72, the feature becomes ACTIVE, forever: - add_new_blocks(manager, 1) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=72, - features=[ - dict( - name='NOP_FEATURE_1', - state='ACTIVE', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=72, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 72 - assert get_ancestor_iteratively_mock.call_count == 0 - calculate_new_state_mock.reset_mock() + # at the beginning, the feature is DEFINED: + add_new_blocks(manager, 10) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=10, + features=[ + dict( + name='NOP_FEATURE_1', + state='DEFINED', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 19, the feature is DEFINED, just before becoming STARTED: + add_new_blocks(manager, 9) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=19, + features=[ + dict( + name='NOP_FEATURE_1', + state='DEFINED', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 20, the feature becomes STARTED: + add_new_blocks(manager, 1) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=20, + features=[ + dict( + name='NOP_FEATURE_1', + state='STARTED', + acceptance=0, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 55, the feature is STARTED, just before becoming MUST_SIGNAL: + add_new_blocks(manager, 35) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=55, + features=[ + dict( + name='NOP_FEATURE_1', + state='STARTED', + acceptance=0, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 56, the feature becomes MUST_SIGNAL: + add_new_blocks(manager, 1) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=56, + features=[ + dict( + name='NOP_FEATURE_1', + state='MUST_SIGNAL', + acceptance=0, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + add_new_blocks(manager, 1, signal_bits=0b1) + + # if we try to propagate a non-signaling block, it is not accepted + non_signaling_block = manager.generate_mining_block() + manager.cpu_mining_service.resolve(non_signaling_block) + non_signaling_block.signal_bits = 0b10 + non_signaling_block.init_static_metadata_from_storage(settings, manager.tx_storage) + + with pytest.raises(BlockMustSignalError): + manager.verification_service.verify(non_signaling_block) + + assert not manager.propagate_tx(non_signaling_block) + + # at block 59, the feature is MUST_SIGNAL, just before becoming LOCKED_IN: + add_new_blocks(manager, num_blocks=2, signal_bits=0b1) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=59, + features=[ + dict( + name='NOP_FEATURE_1', + state='MUST_SIGNAL', + acceptance=0.75, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 60, the feature becomes LOCKED_IN: + add_new_blocks(manager, 1) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=60, + features=[ + dict( + name='NOP_FEATURE_1', + state='LOCKED_IN', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 71, the feature is LOCKED_IN, just before becoming ACTIVE: + add_new_blocks(manager, 11) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=71, + features=[ + dict( + name='NOP_FEATURE_1', + state='LOCKED_IN', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + + # at block 72, the feature becomes ACTIVE, forever: + add_new_blocks(manager, 1) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=72, + features=[ + dict( + name='NOP_FEATURE_1', + state='ACTIVE', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=72, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) def test_reorg(self) -> None: feature_settings = FeatureSettings( @@ -354,12 +305,10 @@ 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( - feature_settings=feature_settings, - feature_service=feature_service, + settings=settings, tx_storage=artifacts.tx_storage ) web_client = StubSite(feature_resource) @@ -569,53 +518,38 @@ 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( - feature_settings=feature_settings, - feature_service=feature_service1, + settings=settings, tx_storage=artifacts1.tx_storage ) web_client = StubSite(feature_resource) - calculate_new_state_mock = Mock(wraps=feature_service1._calculate_new_state) - get_ancestor_iteratively_mock = Mock(wraps=feature_service1._get_ancestor_iteratively) - - with ( - patch.object(FeatureService, '_calculate_new_state', calculate_new_state_mock), - patch.object(FeatureService, '_get_ancestor_iteratively', get_ancestor_iteratively_mock), - ): - assert artifacts1.tx_storage.get_vertices_count() == 3 # genesis vertices in the storage - - # we add 64 blocks so the feature becomes active. It would be active by timeout anyway, - # we just set signal bits to conform with the MUST_SIGNAL phase. - add_new_blocks(manager1, 64, signal_bits=0b1) - self.simulator.run(60) - result = self._get_result(web_client) - assert result == dict( - block_height=64, - features=[ - dict( - name='NOP_FEATURE_1', - state='ACTIVE', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=0, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - # feature states have to be calculated for all blocks in evaluation interval boundaries, - # down to the first one (after genesis), as this is the first run: - assert min(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 4 - # no blocks are voided, so we only use the height index: - assert get_ancestor_iteratively_mock.call_count == 0 - assert artifacts1.tx_storage.get_vertices_count() == 67 - calculate_new_state_mock.reset_mock() + assert artifacts1.tx_storage.get_vertices_count() == 3 # genesis vertices in the storage + + # we add 64 blocks so the feature becomes active. It would be active by timeout anyway, + # we just set signal bits to conform with the MUST_SIGNAL phase. + add_new_blocks(manager1, 64, signal_bits=0b1) + self.simulator.run(60) + result = self._get_result(web_client) + assert result == dict( + block_height=64, + features=[ + dict( + name='NOP_FEATURE_1', + state='ACTIVE', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=0, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + assert artifacts1.tx_storage.get_vertices_count() == 67 manager1.stop() not_none(artifacts1.rocksdb_storage).close() @@ -623,50 +557,37 @@ 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( - feature_settings=feature_settings, - feature_service=feature_service, + settings=settings, tx_storage=artifacts2.tx_storage ) web_client = StubSite(feature_resource) - calculate_new_state_mock = Mock(wraps=feature_service._calculate_new_state) - get_ancestor_iteratively_mock = Mock(wraps=feature_service._get_ancestor_iteratively) - - with ( - patch.object(FeatureService, '_calculate_new_state', calculate_new_state_mock), - patch.object(FeatureService, '_get_ancestor_iteratively', get_ancestor_iteratively_mock), - ): - # the new storage starts populated - assert artifacts2.tx_storage.get_vertices_count() == 67 - self.simulator.run(60) - - result = self._get_result(web_client) - - # the result should be the same as before - assert result == dict( - block_height=64, - features=[ - dict( - name='NOP_FEATURE_1', - state='ACTIVE', - acceptance=None, - threshold=0.75, - start_height=20, - timeout_height=60, - minimum_activation_height=0, - lock_in_on_timeout=True, - version='0.0.0' - ) - ] - ) - # features states are not calculate for any block, as they're all saved: - assert len(self._calculate_new_state_mock_block_height_calls(calculate_new_state_mock)) == 0 - assert get_ancestor_iteratively_mock.call_count == 0 - assert artifacts2.tx_storage.get_vertices_count() == 67 - calculate_new_state_mock.reset_mock() + # the new storage starts populated + assert artifacts2.tx_storage.get_vertices_count() == 67 + self.simulator.run(60) + + result = self._get_result(web_client) + + # the result should be the same as before + assert result == dict( + block_height=64, + features=[ + dict( + name='NOP_FEATURE_1', + state='ACTIVE', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=0, + lock_in_on_timeout=True, + version='0.0.0' + ) + ] + ) + assert artifacts2.tx_storage.get_vertices_count() == 67 class SyncV1MemoryStorageFeatureSimulationTest(unittest.SyncV1Params, BaseMemoryStorageFeatureSimulationTest): diff --git a/tests/others/test_cli_builder.py b/tests/others/test_cli_builder.py index 64e95e208..363f5beff 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 b2caa9099..590ff6120 100644 --- a/tests/resources/feature/test_feature.py +++ b/tests/resources/feature/test_feature.py @@ -16,10 +16,9 @@ import pytest +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_description import FeatureDescription 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 @@ -36,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.ACTIVE, + Feature.NOP_FEATURE_2: FeatureState.STARTED, + }, ) block.set_static_metadata(static_metadata) @@ -44,42 +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_bits_description = Mock(return_value={ - Feature.NOP_FEATURE_1: FeatureDescription(state=FeatureState.DEFINED, criteria=nop_feature_1_criteria), - Feature.NOP_FEATURE_2: FeatureDescription(state=FeatureState.LOCKED_IN, criteria=nop_feature_2_criteria), - }) - - feature_settings = FeatureSettings( + settings = Mock(spec_set=HathorSettings) + settings.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 + 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( - feature_settings=feature_settings, - feature_service=feature_service, + settings=settings, tx_storage=tx_storage ) @@ -126,7 +115,7 @@ def test_get_block_features(web: StubSite) -> None: result = response.result.json_value() expected = dict( signal_bits=[ - dict(bit=1, signal=0, feature="NOP_FEATURE_2", feature_state="LOCKED_IN") + dict(bit=1, signal=0, feature="NOP_FEATURE_2", feature_state="STARTED") ] ) diff --git a/tests/resources/transaction/test_mining.py b/tests/resources/transaction/test_mining.py index e412f9043..078fd9fea 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 61e2512c0..302d74b83 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 @@ -138,25 +137,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 1db260bf2..5aa05f0ef 100644 --- a/tests/tx/test_genesis.py +++ b/tests/tx/test_genesis.py @@ -32,7 +32,7 @@ class GenesisTest(unittest.TestCase): def setUp(self): 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(verifiers=verifiers, settings=self._settings) self.storage = TransactionMemoryStorage() 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 7db911c90..40c417f17 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(verifiers=verifiers, settings=self._settings) def test_deserialize(self): diff --git a/tests/tx/test_verification.py b/tests/tx/test_verification.py index 3fec7556d..0b25e6196 100644 --- a/tests/tx/test_verification.py +++ b/tests/tx/test_verification.py @@ -321,8 +321,6 @@ def test_merge_mined_block_verify_without_storage(self) -> None: verify_data_wrapped = Mock(wraps=self.verifiers.block.verify_data) verify_sigops_output_wrapped = Mock(wraps=self.verifiers.vertex.verify_sigops_output) - verify_aux_pow_wrapped = Mock(wraps=self.verifiers.merge_mined_block.verify_aux_pow) - with ( patch.object(VertexVerifier, 'verify_outputs', verify_outputs_wrapped), patch.object(VertexVerifier, 'verify_pow', verify_pow_wrapped), @@ -331,7 +329,6 @@ def test_merge_mined_block_verify_without_storage(self) -> None: patch.object(VertexVerifier, 'verify_number_of_outputs', verify_number_of_outputs_wrapped), patch.object(BlockVerifier, 'verify_data', verify_data_wrapped), patch.object(VertexVerifier, 'verify_sigops_output', verify_sigops_output_wrapped), - patch.object(MergeMinedBlockVerifier, 'verify_aux_pow', verify_aux_pow_wrapped), ): self.manager.verification_service.verify_without_storage(block) @@ -346,9 +343,6 @@ def test_merge_mined_block_verify_without_storage(self) -> None: verify_data_wrapped.assert_called_once() verify_sigops_output_wrapped.assert_called_once() - # MergeMinedBlock methods - verify_aux_pow_wrapped.assert_called_once() - def test_merge_mined_block_verify(self) -> None: block = self._get_valid_merge_mined_block()