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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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