From 1b1333be23bb3faaf064030b4561028419b8506e Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Wed, 14 Jun 2023 15:40:21 -0300 Subject: [PATCH] feat(feature-activation): implement block ancestor optimization --- hathor/builder/builder.py | 9 +- hathor/cli/run_node.py | 5 +- hathor/feature_activation/feature_service.py | 37 +++- hathor/simulator/simulator.py | 7 +- .../storage/transaction_storage.py | 7 + .../test_feature_service.py | 178 +++++++++++++---- .../test_feature_simulation.py | 189 ++++++++++++++++++ tests/simulation/test_trigger.py | 5 +- 8 files changed, 380 insertions(+), 57 deletions(-) create mode 100644 tests/feature_activation/test_feature_simulation.py diff --git a/hathor/builder/builder.py b/hathor/builder/builder.py index 9ef90dac8..4499b69a6 100644 --- a/hathor/builder/builder.py +++ b/hathor/builder/builder.py @@ -190,7 +190,7 @@ def build(self) -> BuildArtifacts: if self._enable_stratum_server: stratum_factory = self._create_stratum_server(manager) - feature_service = self._create_feature_service() + feature_service = self._create_feature_service(tx_storage) self.artifacts = BuildArtifacts( peer_id=peer_id, @@ -272,8 +272,11 @@ def _create_stratum_server(self, manager: HathorManager) -> StratumFactory: manager.metrics.stratum_factory = stratum_factory return stratum_factory - def _create_feature_service(self) -> FeatureService: - return FeatureService(feature_settings=self._settings.FEATURE_ACTIVATION) + def _create_feature_service(self, tx_storage: TransactionStorage) -> FeatureService: + return FeatureService( + feature_settings=self._settings.FEATURE_ACTIVATION, + tx_storage=tx_storage + ) def _get_or_create_rocksdb_storage(self) -> RocksDBStorage: assert self._rocksdb_path is not None diff --git a/hathor/cli/run_node.py b/hathor/cli/run_node.py index 74f4ef313..f4b74eb04 100644 --- a/hathor/cli/run_node.py +++ b/hathor/cli/run_node.py @@ -156,7 +156,10 @@ def prepare(self, *, register_resources: bool = True) -> None: from hathor.feature_activation.feature_service import FeatureService settings = HathorSettings() - feature_service = FeatureService(feature_settings=settings.FEATURE_ACTIVATION) + feature_service = FeatureService( + feature_settings=settings.FEATURE_ACTIVATION, + tx_storage=self.manager.tx_storage + ) if register_resources: resources_builder = ResourcesBuilder(self.manager, self._args, builder.event_ws_factory, feature_service) diff --git a/hathor/feature_activation/feature_service.py b/hathor/feature_activation/feature_service.py index acce373d5..6566d56bb 100644 --- a/hathor/feature_activation/feature_service.py +++ b/hathor/feature_activation/feature_service.py @@ -18,13 +18,15 @@ 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 class FeatureService: - __slots__ = ('_feature_settings',) + __slots__ = ('_feature_settings', '_tx_storage') - def __init__(self, *, feature_settings: FeatureSettings) -> None: + def __init__(self, *, feature_settings: FeatureSettings, tx_storage: TransactionStorage) -> None: self._feature_settings = feature_settings + self._tx_storage = tx_storage def is_feature_active(self, *, block: Block, feature: Feature) -> bool: """Returns whether a Feature is active at a certain block.""" @@ -45,7 +47,7 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState: 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 - previous_boundary_block = _get_ancestor_at_height(block=block, height=previous_boundary_height) + previous_boundary_block = self._get_ancestor_at_height(block=block, height=previous_boundary_height) previous_state = self.get_state(block=previous_boundary_block, feature=feature) if offset_to_boundary != 0: @@ -133,17 +135,30 @@ def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescript for feature, criteria in self._feature_settings.features.items() } + def _get_ancestor_at_height(self, *, block: Block, height: int) -> Block: + """ + Given a block, returns its ancestor at a specific height. + Uses the height index if the block is in the best blockchain, or search iteratively otherwise. + """ + assert height < block.get_height(), ( + f"ancestor height must be lower than the block's height: {height} >= {block.get_height()}" + ) + + metadata = block.get_metadata() + + if not metadata.voided_by and (ancestor := self._tx_storage.get_transaction_by_height(height)): + assert isinstance(ancestor, Block) + return ancestor + + return _get_ancestor_iteratively(block=block, ancestor_height=height) -def _get_ancestor_at_height(*, block: Block, height: int) -> Block: - """Given a block, returns its ancestor at a specific height.""" - # TODO: there may be more optimized ways of doing this using the height index, - # but what if we're not in the best blockchain? - assert height < block.get_height(), ( - f"ancestor height must be lower than the block's height: {height} >= {block.get_height()}" - ) +def _get_ancestor_iteratively(*, block: Block, ancestor_height: int) -> Block: + """Given a block, returns its ancestor at a specific height by iterating over its ancestors. This is slow.""" + # 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. ancestor = block - while ancestor.get_height() > height: + while ancestor.get_height() > ancestor_height: ancestor = ancestor.get_block_parent() return ancestor diff --git a/hathor/simulator/simulator.py b/hathor/simulator/simulator.py index 7dbb13280..4e523fd02 100644 --- a/hathor/simulator/simulator.py +++ b/hathor/simulator/simulator.py @@ -24,6 +24,7 @@ from hathor.conf import HathorSettings from hathor.daa import TestMode, _set_test_mode from hathor.manager import HathorManager +from hathor.p2p.peer_id import PeerId from hathor.simulator.clock import HeapClock from hathor.simulator.miner.geometric_miner import GeometricMiner from hathor.simulator.tx_generator import RandomTransactionGenerator @@ -144,13 +145,14 @@ def get_default_builder(self) -> Builder: """ return Builder() \ .set_network(self._network) \ + .set_peer_id(PeerId()) \ .set_soft_voided_tx_ids(set()) \ .enable_full_verification() \ .enable_sync_v1() \ .enable_sync_v2() \ .use_memory() - def create_peer(self, builder: Builder) -> HathorManager: + def create_peer(self, builder: Optional[Builder] = None) -> HathorManager: """ Returns a manager from a builder, after configuring it for simulator use. You may get a builder from get_default_builder() for convenience. @@ -158,12 +160,13 @@ def create_peer(self, builder: Builder) -> HathorManager: artifacts = self.create_artifacts(builder) return artifacts.manager - def create_artifacts(self, builder: Builder) -> BuildArtifacts: + def create_artifacts(self, builder: Optional[Builder] = None) -> BuildArtifacts: """ Returns build artifacts from a builder, after configuring it for simulator use. You may get a builder from get_default_builder() for convenience. """ assert self._started, 'Simulator is not started.' + builder = builder or self.get_default_builder() wallet = HDWallet(gap_limit=2) wallet._manually_initialize() diff --git a/hathor/transaction/storage/transaction_storage.py b/hathor/transaction/storage/transaction_storage.py index 7bb66e300..f87ed3a0e 100644 --- a/hathor/transaction/storage/transaction_storage.py +++ b/hathor/transaction/storage/transaction_storage.py @@ -561,6 +561,13 @@ def get_transaction(self, hash_bytes: bytes) -> BaseTransaction: self.post_get_validation(tx) return tx + def get_transaction_by_height(self, height: int) -> Optional[BaseTransaction]: + """Returns a transaction from the height index. This is fast.""" + assert self.indexes is not None + ancestor_hash = self.indexes.height.get(height) + + return None if ancestor_hash is None else self.get_transaction(ancestor_hash) + def get_metadata(self, hash_bytes: bytes) -> Optional[TransactionMetadata]: """Returns the transaction metadata with hash `hash_bytes`. diff --git a/tests/feature_activation/test_feature_service.py b/tests/feature_activation/test_feature_service.py index fb8a8377f..976e35c6e 100644 --- a/tests/feature_activation/test_feature_service.py +++ b/tests/feature_activation/test_feature_service.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import cast from unittest.mock import Mock, patch import pytest from hathor.conf import HathorSettings -from hathor.feature_activation import feature_service from hathor.feature_activation.feature import Feature from hathor.feature_activation.feature_service import FeatureService from hathor.feature_activation.model.criteria import Criteria @@ -28,8 +28,9 @@ from hathor.transaction.storage import TransactionStorage -@pytest.fixture -def block_mocks() -> list[Block]: +def _get_blocks_and_storage() -> tuple[list[Block], TransactionStorage]: + settings = HathorSettings() + genesis_hash = settings.GENESIS_BLOCK_HASH blocks: list[Block] = [] feature_activation_bits = [ 0b0000, # 0: boundary block @@ -60,31 +61,52 @@ def block_mocks() -> list[Block]: 0b0000, # 20: boundary block 0b0000, ] + storage = Mock() + storage.get_metadata = Mock(return_value=None) for i, bits in enumerate(feature_activation_bits): - settings = HathorSettings() - genesis_hash = settings.GENESIS_BLOCK_HASH - block_hash = genesis_hash if i == 0 else b'some_hash' - - storage = Mock(spec_set=TransactionStorage) - storage.get_metadata = Mock(return_value=None) - + block_hash = genesis_hash if i == 0 else int.to_bytes(i, length=1, byteorder='big') block = Block(hash=block_hash, storage=storage, signal_bits=bits) blocks.append(block) + parent_hash = blocks[i - 1].hash + assert parent_hash is not None + block.parents = [parent_hash] + + block_by_hash = {block.hash: block for block in blocks} + storage.get_transaction = Mock(side_effect=lambda hash_bytes: block_by_hash[hash_bytes]) + storage.get_transaction_by_height = Mock(side_effect=lambda height: blocks[height]) + + return blocks, storage + - get_block_parent_mock = Mock(return_value=blocks[i - 1]) - setattr(block, 'get_block_parent', get_block_parent_mock) +@pytest.fixture +def block_mocks() -> list[Block]: + blocks, _ = _get_blocks_and_storage() return blocks @pytest.fixture -def service() -> FeatureService: - feature_settings = FeatureSettings( +def tx_storage() -> TransactionStorage: + _, tx_storage = _get_blocks_and_storage() + + return tx_storage + + +@pytest.fixture +def feature_settings() -> FeatureSettings: + return FeatureSettings( evaluation_interval=4, default_threshold=3 ) - service = FeatureService(feature_settings=feature_settings) + + +@pytest.fixture +def service(feature_settings: FeatureSettings, tx_storage: TransactionStorage) -> FeatureService: + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) return service @@ -115,6 +137,7 @@ def test_get_state_first_interval(block_mocks: list[Block], service: FeatureServ ) def test_get_state_from_defined( block_mocks: list[Block], + tx_storage: TransactionStorage, block_height: int, start_height: int, expected_state: FeatureState @@ -130,7 +153,10 @@ def test_get_state_from_defined( ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -140,7 +166,12 @@ def test_get_state_from_defined( @pytest.mark.parametrize('block_height', [8, 9, 10, 11, 12, 13]) @pytest.mark.parametrize('timeout_height', [4, 8]) -def test_get_state_from_started_to_failed(block_mocks: list[Block], block_height: int, timeout_height: int) -> None: +def test_get_state_from_started_to_failed( + block_mocks: list[Block], + tx_storage: TransactionStorage, + block_height: int, + timeout_height: int, +) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -153,7 +184,10 @@ def test_get_state_from_started_to_failed(block_mocks: list[Block], block_height ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -166,6 +200,7 @@ def test_get_state_from_started_to_failed(block_mocks: list[Block], block_height @pytest.mark.parametrize('minimum_activation_height', [0, 4, 8]) def test_get_state_from_started_to_active_on_timeout( block_mocks: list[Block], + tx_storage: TransactionStorage, block_height: int, timeout_height: int, minimum_activation_height: int @@ -183,7 +218,10 @@ def test_get_state_from_started_to_active_on_timeout( ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -196,6 +234,7 @@ def test_get_state_from_started_to_active_on_timeout( @pytest.mark.parametrize('default_threshold', [0, 1, 2, 3]) def test_get_state_from_started_to_active_on_default_threshold( block_mocks: list[Block], + tx_storage: TransactionStorage, block_height: int, minimum_activation_height: int, default_threshold: int @@ -214,7 +253,10 @@ def test_get_state_from_started_to_active_on_default_threshold( ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -227,6 +269,7 @@ def test_get_state_from_started_to_active_on_default_threshold( @pytest.mark.parametrize('custom_threshold', [0, 1, 2, 3]) def test_get_state_from_started_to_active_on_custom_threshold( block_mocks: list[Block], + tx_storage: TransactionStorage, block_height: int, minimum_activation_height: int, custom_threshold: int @@ -244,7 +287,10 @@ def test_get_state_from_started_to_active_on_custom_threshold( ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -263,6 +309,7 @@ def test_get_state_from_started_to_active_on_custom_threshold( ) def test_get_state_from_started_to_started( block_mocks: list[Block], + tx_storage: TransactionStorage, block_height: int, activate_on_timeout: bool, timeout_height: int, @@ -281,7 +328,10 @@ def test_get_state_from_started_to_started( ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -290,7 +340,7 @@ def test_get_state_from_started_to_started( @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) -def test_get_state_from_active(block_mocks: list[Block], block_height: int) -> None: +def test_get_state_from_active(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -303,7 +353,10 @@ def test_get_state_from_active(block_mocks: list[Block], block_height: int) -> N ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -312,7 +365,7 @@ def test_get_state_from_active(block_mocks: list[Block], block_height: int) -> N @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) -def test_is_feature_active(block_mocks: list[Block], block_height: int) -> None: +def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -325,7 +378,10 @@ def test_is_feature_active(block_mocks: list[Block], block_height: int) -> None: ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.is_feature_active(block=block, feature=Feature.NOP_FEATURE_1) @@ -334,7 +390,7 @@ def test_is_feature_active(block_mocks: list[Block], block_height: int) -> None: @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) -def test_get_state_from_failed(block_mocks: list[Block], block_height: int) -> None: +def test_get_state_from_failed(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -346,7 +402,10 @@ def test_get_state_from_failed(block_mocks: list[Block], block_height: int) -> N ) } ) - service = FeatureService(feature_settings=feature_settings) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) block = block_mocks[block_height] result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -363,16 +422,19 @@ def test_get_state_undefined_feature(block_mocks: list[Block], service: FeatureS assert str(e.value) == f"Criteria not defined for feature '{Feature.NOP_FEATURE_1}'." -def test_get_bits_description(): - criteria_mock_1 = Criteria.construct() - criteria_mock_2 = Criteria.construct() +def test_get_bits_description(tx_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) + service = FeatureService( + feature_settings=feature_settings, + tx_storage=tx_storage + ) def get_state(self: FeatureService, *, block: Block, feature: Feature) -> FeatureState: states = { @@ -402,11 +464,18 @@ def get_state(self: FeatureService, *, block: Block, feature: Feature) -> Featur (0, 0), ] ) -def test_get_ancestor_at_height_invalid(block_mocks: list[Block], block_height: int, ancestor_height: int) -> None: +def test_get_ancestor_at_height_invalid( + feature_settings: FeatureSettings, + block_mocks: list[Block], + tx_storage: TransactionStorage, + block_height: int, + ancestor_height: int +) -> None: + service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) block = block_mocks[block_height] with pytest.raises(AssertionError) as e: - feature_service._get_ancestor_at_height(block=block, height=ancestor_height) + service._get_ancestor_at_height(block=block, height=ancestor_height) assert str(e.value) == ( f"ancestor height must be lower than the block's height: {ancestor_height} >= {block_height}" @@ -424,8 +493,45 @@ def test_get_ancestor_at_height_invalid(block_mocks: list[Block], block_height: (1, 0), ] ) -def test_get_ancestor_at_height(block_mocks: list[Block], block_height: int, ancestor_height: int) -> None: +def test_get_ancestor_at_height( + feature_settings: FeatureSettings, + block_mocks: list[Block], + tx_storage: TransactionStorage, + block_height: int, + ancestor_height: int +) -> None: + service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + block = block_mocks[block_height] + result = service._get_ancestor_at_height(block=block, height=ancestor_height) + + assert result == block_mocks[ancestor_height] + assert result.get_height() == ancestor_height + assert cast(Mock, tx_storage.get_transaction_by_height).call_count == 1 + + +@pytest.mark.parametrize( + ['block_height', 'ancestor_height'], + [ + (21, 20), + (21, 10), + (21, 0), + (15, 10), + (15, 0), + (1, 0), + ] +) +def test_get_ancestor_at_height_voided( + feature_settings: FeatureSettings, + block_mocks: list[Block], + tx_storage: TransactionStorage, + block_height: int, + ancestor_height: int +) -> None: + service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) block = block_mocks[block_height] - result = feature_service._get_ancestor_at_height(block=block, height=ancestor_height) + block.get_metadata().voided_by = {b'some'} + result = service._get_ancestor_at_height(block=block, height=ancestor_height) + assert result == block_mocks[ancestor_height] assert result.get_height() == ancestor_height + assert cast(Mock, tx_storage.get_transaction_by_height).call_count == 0 diff --git a/tests/feature_activation/test_feature_simulation.py b/tests/feature_activation/test_feature_simulation.py new file mode 100644 index 000000000..a6b79a9f7 --- /dev/null +++ b/tests/feature_activation/test_feature_simulation.py @@ -0,0 +1,189 @@ +# 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 Any +from unittest.mock import Mock, patch + +from hathor.feature_activation import feature_service +from hathor.feature_activation.feature import Feature +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 +from hathor.simulator.trigger import StopAfterNMinedBlocks +from tests import unittest +from tests.resources.base_resource import StubSite +from tests.simulation.base import SimulatorTestCase + +_FEATURE_SETTINGS = FeatureSettings( + evaluation_interval=4, + max_signal_bits=4, + default_threshold=3, + features={ + Feature.NOP_FEATURE_1: Criteria( + bit=0, + start_height=20, + timeout_height=60, + activate_on_timeout=True, + version='0.0.0' + ) + } +) + + +class BaseTestFeatureSimulation(SimulatorTestCase): + def setUp(self): + super().setUp() + artifacts = self.simulator.create_artifacts() + manager = artifacts.manager + manager.allow_mining_without_peers() + + self.feature_service = artifacts.feature_service + self.feature_service._feature_settings = _FEATURE_SETTINGS + feature_resource = FeatureResource( + feature_settings=_FEATURE_SETTINGS, + feature_service=self.feature_service, + tx_storage=manager.tx_storage + ) + self.web_client = StubSite(feature_resource) + + self.miner = self.simulator.create_miner(manager, hashpower=1e6) + self.miner.start() + + def _get_result_after(self, *, n_blocks: int) -> dict[str, Any]: + trigger = StopAfterNMinedBlocks(self.miner, quantity=n_blocks) + self.simulator.run(3600, trigger=trigger) + + response = self.web_client.get('feature') + result = response.result.json_value() + + del result['block_hash'] # we don't assert the block hash because it's not always the same + + return result + + def test_feature(self): + """ + Test that a feature goes through all possible states in the correct block heights, and also assert internal + method call counts to make sure we're executing it in the most performatic way. + """ + get_ancestor_iteratively_mock = Mock(wraps=feature_service._get_ancestor_iteratively) + + with patch.object(feature_service, '_get_ancestor_iteratively', get_ancestor_iteratively_mock): + # at the beginning, the feature is DEFINED + assert self._get_result_after(n_blocks=10) == 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=0, + activate_on_timeout=True, + version='0.0.0' + ) + ] + ) + # no blocks are voided, so we only use the height index: + assert get_ancestor_iteratively_mock.call_count == 0 + + # at block 19, the feature is DEFINED, just before becoming STARTED + assert self._get_result_after(n_blocks=9) == 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=0, + activate_on_timeout=True, + version='0.0.0' + ) + ] + ) + assert get_ancestor_iteratively_mock.call_count == 0 + + # at block 20, the feature becomes STARTED + assert self._get_result_after(n_blocks=1) == 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=0, + activate_on_timeout=True, + version='0.0.0' + ) + ] + ) + assert get_ancestor_iteratively_mock.call_count == 0 + + # at block 39, the feature is STARTED, just before becoming ACTIVE + assert self._get_result_after(n_blocks=39) == dict( + block_height=59, + features=[ + dict( + name='NOP_FEATURE_1', + state='STARTED', + acceptance=0, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=0, + activate_on_timeout=True, + version='0.0.0' + ) + ] + ) + assert get_ancestor_iteratively_mock.call_count == 0 + + # at block 60, the feature becomes ACTIVE, forever + assert self._get_result_after(n_blocks=1) == dict( + block_height=60, + features=[ + dict( + name='NOP_FEATURE_1', + state='ACTIVE', + acceptance=None, + threshold=0.75, + start_height=20, + timeout_height=60, + minimum_activation_height=0, + activate_on_timeout=True, + version='0.0.0' + ) + ] + ) + assert get_ancestor_iteratively_mock.call_count == 0 + + +class SyncV1BaseTestFeatureSimulation(unittest.SyncV1Params, BaseTestFeatureSimulation): + __test__ = True + + +class SyncV2BaseTestFeatureSimulation(unittest.SyncV2Params, BaseTestFeatureSimulation): + __test__ = True + + +# sync-bridge should behave like sync-v2 +class SyncBridgeBaseTestFeatureSimulation(unittest.SyncBridgeParams, SyncV2BaseTestFeatureSimulation): + __test__ = True diff --git a/tests/simulation/test_trigger.py b/tests/simulation/test_trigger.py index 728b04a96..cb7ddb1ec 100644 --- a/tests/simulation/test_trigger.py +++ b/tests/simulation/test_trigger.py @@ -1,4 +1,3 @@ -from hathor.p2p.peer_id import PeerId from hathor.simulator import Simulator from hathor.simulator.trigger import StopAfterMinimumBalance, StopAfterNMinedBlocks from tests import unittest @@ -11,9 +10,7 @@ def setUp(self): self.simulator = Simulator() self.simulator.start() - peer_id = PeerId() - builder = self.simulator.get_default_builder().set_peer_id(peer_id) - self.manager1 = self.simulator.create_peer(builder) + self.manager1 = self.simulator.create_peer() self.manager1.allow_mining_without_peers() print('-' * 30)