Skip to content

Commit

Permalink
feat(feature-activation): automatically enable support in MUST_SIGNAL
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Apr 2, 2024
1 parent 3ff748d commit 826dc0c
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 2 deletions.
7 changes: 7 additions & 0 deletions hathor/feature_activation/bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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'
Expand Down
10 changes: 8 additions & 2 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
# 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
from hathor.feature_activation.model.feature_state import FeatureState
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

Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions tests/feature_activation/test_bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
20 changes: 20 additions & 0 deletions tests/feature_activation/test_feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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'}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 826dc0c

Please sign in to comment.