From bbc930a7c330beaf7b971bee1f9b0db3198320e4 Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Wed, 17 Sep 2025 18:20:47 -0300 Subject: [PATCH] fix(nano): initialization of block root ids --- hathor/consensus/block_consensus.py | 35 +++---------- .../test_nano_feature_activation.py | 49 +++++++++++++++++-- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/hathor/consensus/block_consensus.py b/hathor/consensus/block_consensus.py index 3c23716d4e..8607e3c7db 100644 --- a/hathor/consensus/block_consensus.py +++ b/hathor/consensus/block_consensus.py @@ -71,8 +71,11 @@ def update_consensus(self, block: Block) -> None: assert self.context.nc_events is None self.context.nc_events = [] self.update_voided_info(block) - if self._settings.ENABLE_NANO_CONTRACTS: + + if self._should_execute_nano(block): self.execute_nano_contracts(block) + else: + self._nc_initialize_empty(block) def _nc_initialize_empty(self, block: Block) -> None: """Initialize a block with an empty contract trie.""" @@ -149,29 +152,10 @@ def _should_execute_nano(self, block: Block) -> bool: vertex=parent, feature=Feature.NANO_CONTRACTS, ) - is_active_on_block = self.feature_service.is_feature_active( - vertex=block, - feature=Feature.NANO_CONTRACTS, - ) - match is_active_on_parent, is_active_on_block: - case False, False: - # Nano is not active, nothing to execute. - return False - case False, True: - # This is the first active block, so we initialize it and return False - # because there's nothing to execute on the first active block. - self._nc_initialize_empty(block) - return False - case True, False: # pragma: no cover - raise AssertionError('unreachable') - case True, True: - # All set, proceed with execution. - return True - case _: # pragma: no cover - raise AssertionError('unreachable') - - case NanoContractsSetting.DISABLED: # pragma: no cover - raise AssertionError('unreachable') + return is_active_on_parent + + case NanoContractsSetting.DISABLED: + return False case _: # pragma: no cover assert_never(self._settings.ENABLE_NANO_CONTRACTS) @@ -190,9 +174,6 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None: self._nc_initialize_empty(block) return - if not self._should_execute_nano(block): - return - meta = block.get_metadata() assert not meta.voided_by assert meta.nc_block_root_id is None diff --git a/tests/nanocontracts/test_nano_feature_activation.py b/tests/nanocontracts/test_nano_feature_activation.py index 1b54e7589c..b6862400c6 100644 --- a/tests/nanocontracts/test_nano_feature_activation.py +++ b/tests/nanocontracts/test_nano_feature_activation.py @@ -30,13 +30,15 @@ class MyBluprint(Blueprint): + a: int + @public def initialize(self, ctx: Context) -> None: - pass + self.a = 123 @public def nop(self, ctx: Context) -> None: - pass + self.a = 456 class TestNanoFeatureActivation(unittest.TestCase): @@ -74,12 +76,16 @@ def setUp(self) -> None: assert self.manager.tx_storage.nc_catalog is not None self.manager.tx_storage.nc_catalog.blueprints[self.blueprint_id] = MyBluprint + empty_block_storage = self.manager.consensus_algorithm.nc_storage_factory.get_empty_block_storage() + empty_block_storage.commit() + self.empty_root_id = empty_block_storage.get_root_id() + def test_activation(self) -> None: private_key = unittest.OCB_TEST_PRIVKEY.hex() password = unittest.OCB_TEST_PASSWORD.hex() artifacts = self.dag_builder.build_from_str(f''' blockchain genesis b[1..13] - blockchain b10 a[11..12] + blockchain b10 a[11..13] b10 < dummy < b11 nc1.nc_id = "{self.blueprint_id.hex()}" @@ -96,10 +102,13 @@ def test_activation(self) -> None: a11.weight = 10 b13 < a11 + + nc1 <-- a13 + ocb1 <-- a13 ''') - b3, b4, b7, b8, b11, b12, b13, a11 = artifacts.get_typed_vertices( - ('b3', 'b4', 'b7', 'b8', 'b11', 'b12', 'b13', 'a11'), + b3, b4, b7, b8, b11, b12, b13, a11, a12, a13 = artifacts.get_typed_vertices( + ('b3', 'b4', 'b7', 'b8', 'b11', 'b12', 'b13', 'a11', 'a12', 'a13'), Block, ) nc1, ocb1 = artifacts.get_typed_vertices(('nc1', 'ocb1'), Transaction) @@ -126,6 +135,8 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to='b11') assert self.feature_service.get_state(block=b11, feature=Feature.NANO_CONTRACTS) is FeatureState.LOCKED_IN + assert b11.get_metadata().nc_block_root_id == self.empty_root_id + # At this point, the feature is not active, so the nc txs are rejected on the mempool. msg = 'full validation failed: Header `NanoHeader` not supported by `Transaction`' with pytest.raises(InvalidNewTransaction, match=msg): @@ -142,6 +153,9 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to='b12') assert self.feature_service.get_state(block=b12, feature=Feature.NANO_CONTRACTS) is FeatureState.ACTIVE + assert b11.get_metadata().nc_block_root_id == self.empty_root_id + assert b12.get_metadata().nc_block_root_id == self.empty_root_id + # Now, the nc txs are accepted on the mempool. artifacts.propagate_with(self.manager, up_to='nc1') assert nc1.get_metadata().validation.is_valid() @@ -154,6 +168,10 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to='b13') assert nc1.get_metadata().nc_execution is NCExecutionState.SUCCESS + assert b11.get_metadata().nc_block_root_id == self.empty_root_id + assert b12.get_metadata().nc_block_root_id == self.empty_root_id + assert b13.get_metadata().nc_block_root_id not in (self.empty_root_id, None) + artifacts.propagate_with(self.manager, up_to='a11') assert a11.get_metadata().validation.is_valid() assert a11.get_metadata().voided_by is None @@ -162,6 +180,11 @@ def test_activation(self) -> None: assert ocb1.get_metadata().validation.is_invalid() assert ocb1.get_metadata().validation.is_invalid() + assert b11.get_metadata().nc_block_root_id == self.empty_root_id + assert b12.get_metadata().nc_block_root_id == self.empty_root_id + assert b13.get_metadata().nc_block_root_id not in (self.empty_root_id, None) + assert a11.get_metadata().nc_block_root_id == self.empty_root_id + # The nc txs are removed from the mempool. assert not self.manager.tx_storage.transaction_exists(b13.hash) assert not self.manager.tx_storage.transaction_exists(nc1.hash) @@ -171,6 +194,13 @@ def test_activation(self) -> None: # The nc txs are re-accepted on the mempool. artifacts.propagate_with(self.manager, up_to='a12') + assert self.feature_service.get_state(block=a12, feature=Feature.NANO_CONTRACTS) is FeatureState.ACTIVE + + assert b11.get_metadata().nc_block_root_id == self.empty_root_id + assert b12.get_metadata().nc_block_root_id == self.empty_root_id + assert b13.get_metadata().nc_block_root_id not in (self.empty_root_id, None) + assert a11.get_metadata().nc_block_root_id == self.empty_root_id + assert a12.get_metadata().nc_block_root_id == self.empty_root_id nc1._metadata = None self.vertex_handler.on_new_relayed_vertex(nc1) @@ -185,3 +215,12 @@ def test_activation(self) -> None: assert ocb1.get_metadata().voided_by is None assert self.manager.tx_storage.transaction_exists(ocb1.hash) assert ocb1 in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) + + artifacts.propagate_with(self.manager, up_to='a13') + + assert b11.get_metadata().nc_block_root_id == self.empty_root_id + assert b12.get_metadata().nc_block_root_id == self.empty_root_id + assert b13.get_metadata().nc_block_root_id not in (self.empty_root_id, None) + assert a11.get_metadata().nc_block_root_id == self.empty_root_id + assert a12.get_metadata().nc_block_root_id == self.empty_root_id + assert a13.get_metadata().nc_block_root_id not in (self.empty_root_id, None)