diff --git a/hathor/feature_activation/bit_signaling_service.py b/hathor/feature_activation/bit_signaling_service.py index b3da0d394..639eb1a5c 100644 --- a/hathor/feature_activation/bit_signaling_service.py +++ b/hathor/feature_activation/bit_signaling_service.py @@ -56,6 +56,7 @@ def __init__( self._feature_storage = feature_storage self._validate_support_intersection() + self._feature_service.bit_signaling_service = self def start(self) -> None: """ @@ -136,6 +137,12 @@ def remove_feature_support(self, feature: Feature) -> None: self._support_features.discard(feature) self._not_support_features.add(feature) + def on_must_signal(self, feature: Feature) -> None: + """ + When the MUST_SIGNAL phase is reached, feature support is automatically enabled. + """ + self.add_feature_support(feature) + def _log_signal_bits(self, feature: Feature, enable_bit: bool, support: bool, not_support: bool) -> None: """Generate info log for a feature's signal.""" signal = 'enabled' if enable_bit else 'disabled' diff --git a/hathor/feature_activation/feature_service.py b/hathor/feature_activation/feature_service.py index f02195cec..caadb62fb 100644 --- a/hathor/feature_activation/feature_service.py +++ b/hathor/feature_activation/feature_service.py @@ -13,7 +13,7 @@ # limitations under the License. from dataclasses import dataclass -from typing import TYPE_CHECKING, TypeAlias +from typing import TYPE_CHECKING, Optional, TypeAlias from hathor.feature_activation.feature import Feature from hathor.feature_activation.model.feature_description import FeatureDescription @@ -21,6 +21,7 @@ from hathor.feature_activation.settings import Settings as FeatureSettings if TYPE_CHECKING: + from hathor.feature_activation.bit_signaling_service import BitSignalingService from hathor.transaction import Block from hathor.transaction.storage import TransactionStorage @@ -41,11 +42,12 @@ class BlockIsMissingSignal: class FeatureService: - __slots__ = ('_feature_settings', '_tx_storage') + __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 def is_feature_active(self, *, block: 'Block', feature: Feature) -> bool: """Returns whether a Feature is active at a certain block.""" @@ -113,6 +115,10 @@ def get_state(self, *, block: 'Block', feature: Feature) -> FeatureState: 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) diff --git a/tests/feature_activation/test_bit_signaling_service.py b/tests/feature_activation/test_bit_signaling_service.py index 0a929ffaa..930ca39f2 100644 --- a/tests/feature_activation/test_bit_signaling_service.py +++ b/tests/feature_activation/test_bit_signaling_service.py @@ -286,3 +286,35 @@ def get_bits_description_mock(block: Block) -> dict[Feature, FeatureDescription] best_block_hash='abc', non_signaling_features=non_signaling_features, ) + + +def test_on_must_signal_not_supported() -> None: + service = BitSignalingService( + feature_settings=Mock(), + feature_service=Mock(), + tx_storage=Mock(), + support_features=set(), + not_support_features={Feature.NOP_FEATURE_1}, + feature_storage=Mock(), + ) + + service.on_must_signal(feature=Feature.NOP_FEATURE_1) + + assert service._support_features == {Feature.NOP_FEATURE_1} + assert service._not_support_features == set() + + +def test_on_must_signal_supported() -> None: + service = BitSignalingService( + feature_settings=Mock(), + feature_service=Mock(), + tx_storage=Mock(), + support_features=set(), + not_support_features=set(), + feature_storage=Mock(), + ) + + service.on_must_signal(feature=Feature.NOP_FEATURE_1) + + assert service._support_features == {Feature.NOP_FEATURE_1} + assert service._not_support_features == set() diff --git a/tests/feature_activation/test_feature_service.py b/tests/feature_activation/test_feature_service.py index a66af95dc..60c76d8bc 100644 --- a/tests/feature_activation/test_feature_service.py +++ b/tests/feature_activation/test_feature_service.py @@ -119,6 +119,7 @@ def service(feature_settings: FeatureSettings, tx_storage: TransactionStorage) - feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() return service @@ -169,6 +170,7 @@ def test_get_state_from_defined( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -200,6 +202,7 @@ def test_get_state_from_started_to_failed( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -231,11 +234,13 @@ def test_get_state_from_started_to_must_signal_on_timeout( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, 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]) @@ -263,6 +268,7 @@ def test_get_state_from_started_to_locked_in_on_default_threshold( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -294,6 +300,7 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -333,6 +340,7 @@ def test_get_state_from_started_to_started( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -362,6 +370,7 @@ def test_get_state_from_must_signal_to_locked_in( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -394,6 +403,7 @@ def test_get_state_from_locked_in_to_active( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -426,6 +436,7 @@ def test_get_state_from_locked_in_to_locked_in( feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -451,6 +462,7 @@ def test_get_state_from_active(block_mocks: list[Block], tx_storage: Transaction feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -473,6 +485,7 @@ def test_caching_mechanism(block_mocks: list[Block], tx_storage: TransactionStor } ) service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service.bit_signaling_service = Mock() block = block_mocks[block_height] calculate_new_state_mock = Mock(wraps=service._calculate_new_state) @@ -507,6 +520,7 @@ def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStor feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.is_feature_active(block=block, feature=Feature.NOP_FEATURE_1) @@ -531,6 +545,7 @@ def test_get_state_from_failed(block_mocks: list[Block], tx_storage: Transaction feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -559,6 +574,7 @@ def test_get_bits_description(tx_storage: TransactionStorage) -> None: feature_settings=feature_settings, tx_storage=tx_storage ) + service.bit_signaling_service = Mock() def get_state(self: FeatureService, *, block: Block, feature: Feature) -> FeatureState: states = { @@ -596,6 +612,7 @@ def test_get_ancestor_at_height_invalid( ancestor_height: int ) -> None: service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service.bit_signaling_service = Mock() block = block_mocks[block_height] with pytest.raises(AssertionError) as e: @@ -625,6 +642,7 @@ def test_get_ancestor_at_height( ancestor_height: int ) -> None: service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) @@ -653,6 +671,7 @@ def test_get_ancestor_at_height_voided( ancestor_height: int ) -> None: service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service.bit_signaling_service = Mock() block = block_mocks[block_height] parent_block = block_mocks[block_height - 1] parent_block.get_metadata().voided_by = {b'some'} @@ -711,6 +730,7 @@ def test_check_must_signal( } ) service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service.bit_signaling_service = Mock() block = block_mocks[block_height] result = service.is_signaling_mandatory_features(block)