diff --git a/hathor/conf/mainnet.py b/hathor/conf/mainnet.py index fedb8f6fc..38b9797d0 100644 --- a/hathor/conf/mainnet.py +++ b/hathor/conf/mainnet.py @@ -234,12 +234,24 @@ # Expected to be reached around Tuesday, 2025-08-12 17:39:56 GMT # Right now the best block is 5_748_286 at Wednesday, 2025-08-06 16:02:56 GMT start_height=5_765_760, - timeout_height=5_967_360, # N + 10 * 20160 (10 weeks after the start) + timeout_height=6_350_400, # N + 10 * 20160 (10 weeks after the start) minimum_activation_height=0, lock_in_on_timeout=False, version='0.64.0', signal_support_by_default=True, ), + Feature.NANO_CONTRACTS: Criteria( + bit=1, + # N = 5_765_760 + # Expected to be reached around Tuesday, 2025-08-12 17:39:56 GMT + # Right now the best block is 5_748_286 at Wednesday, 2025-08-06 16:02:56 GMT + start_height=5_947_200, + timeout_height=6_350_400, # N + 10 * 20160 (10 weeks after the start) + minimum_activation_height=6_027_840, + lock_in_on_timeout=False, + version='0.67.0', + signal_support_by_default=True, + ), } ) ) diff --git a/hathor/consensus/consensus.py b/hathor/consensus/consensus.py index 1384df9c6..b963287df 100644 --- a/hathor/consensus/consensus.py +++ b/hathor/consensus/consensus.py @@ -342,6 +342,7 @@ def _compute_vertices_that_became_invalid( lambda tx: self._reward_lock_mempool_rule(tx, new_best_height), lambda tx: self._unknown_contract_mempool_rule(tx), lambda tx: self._nano_activation_rule(storage, tx), + self._checkdatasig_count_rule, ) # From the mempool origin, find the leftmost mempool txs that are invalid. @@ -408,16 +409,44 @@ def _unknown_contract_mempool_rule(self, tx: Transaction) -> bool: return True def _nano_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool: - """Check whether a nano or OCB tx became invalid because the reorg changed the feature activation state.""" + """Check whether a tx became invalid because the reorg changed the nano feature activation state.""" from hathor.nanocontracts import OnChainBlueprint from hathor.nanocontracts.utils import is_nano_active + from hathor.transaction.token_creation_tx import TokenCreationTransaction + from hathor.transaction.token_info import TokenVersion best_block = storage.get_best_block() if is_nano_active(settings=self._settings, block=best_block, feature_service=self.feature_service): # When nano is active, this rule has no effect. return True - return not tx.is_nano_contract() and not isinstance(tx, OnChainBlueprint) + # The nano feature activation is actually used to enable 4 use cases: + + if tx.is_nano_contract(): + return False + + if isinstance(tx, OnChainBlueprint): + return False + + if isinstance(tx, TokenCreationTransaction) and tx.token_version == TokenVersion.FEE: + return False + + if tx.has_fees(): + return False + + return True + + def _checkdatasig_count_rule(self, tx: Transaction) -> bool: + """Check whether a tx became invalid because the reorg changed the checkdatasig feature activation state.""" + from hathor.verification.vertex_verifier import VertexVerifier + + # Any exception in the sigops verification will be considered + # a fail and the tx will be removed from the mempool. + try: + VertexVerifier._verify_sigops_output(settings=self._settings, vertex=tx, enable_checkdatasig_count=True) + except Exception: + return False + return True def _sorted_affected_txs(affected_txs: set[BaseTransaction]) -> list[BaseTransaction]: diff --git a/hathor/verification/vertex_verifier.py b/hathor/verification/vertex_verifier.py index 2ee89700c..04d7d1a86 100644 --- a/hathor/verification/vertex_verifier.py +++ b/hathor/verification/vertex_verifier.py @@ -180,11 +180,25 @@ def verify_number_of_outputs(self, vertex: BaseTransaction) -> None: raise TooManyOutputs('Maximum number of outputs exceeded') def verify_sigops_output(self, vertex: BaseTransaction, enable_checkdatasig_count: bool = True) -> None: + """Alias to `_verify_sigops_output` for compatibility.""" + self._verify_sigops_output( + settings=self._settings, + vertex=vertex, + enable_checkdatasig_count=enable_checkdatasig_count, + ) + + @staticmethod + def _verify_sigops_output( + *, + settings: HathorSettings, + vertex: BaseTransaction, + enable_checkdatasig_count: bool, + ) -> None: """ Count sig operations on all outputs and verify that the total sum is below the limit """ from hathor.transaction.scripts import SigopCounter - max_multisig_pubkeys = self._settings.MAX_MULTISIG_PUBKEYS + max_multisig_pubkeys = settings.MAX_MULTISIG_PUBKEYS counter = SigopCounter( max_multisig_pubkeys=max_multisig_pubkeys, enable_checkdatasig_count=enable_checkdatasig_count, @@ -195,7 +209,7 @@ def verify_sigops_output(self, vertex: BaseTransaction, enable_checkdatasig_coun for tx_output in vertex.outputs: n_txops += counter.get_sigops_count(tx_output.script) - if n_txops > self._settings.MAX_TX_SIGOPS_OUTPUT: + if n_txops > settings.MAX_TX_SIGOPS_OUTPUT: raise TooManySigOps('TX[{}]: Maximum number of sigops for all outputs exceeded ({})'.format( vertex.hash_hex, n_txops)) diff --git a/tests/nanocontracts/test_nano_feature_activation.py b/tests/nanocontracts/test_nano_feature_activation.py index 8945a535a..075daedb3 100644 --- a/tests/nanocontracts/test_nano_feature_activation.py +++ b/tests/nanocontracts/test_nano_feature_activation.py @@ -95,13 +95,18 @@ def test_activation(self) -> None: ocb1.ocb_password = "{password}" ocb1.ocb_code = test_blueprint1.py, TestBlueprint1 - b12 < nc1 < ocb1 + FBT.token_version = fee + FBT.fee = 1 HTR + + tx1.out[0] = 123 FBT + tx1.fee = 1 HTR + + b12 < nc1 < ocb1 < FBT < tx1 < b13 < a11 nc1 <-- b13 ocb1 <-- b13 - a11.weight = 10 - b13 < a11 + a11.weight = 20 nc1 <-- a13 ocb1 <-- a13 @@ -111,7 +116,7 @@ def test_activation(self) -> None: ('b3', 'b4', 'b7', 'b8', 'b11', 'b12', 'b13', 'a11', 'a12', 'a13'), Block, ) - nc1, ocb1 = artifacts.get_typed_vertices(('nc1', 'ocb1'), Transaction) + nc1, ocb1, fbt, tx1 = artifacts.get_typed_vertices(('nc1', 'ocb1', 'FBT', 'tx1'), Transaction) artifacts.propagate_with(self.manager, up_to='b3') assert self.feature_service.get_state(block=b3, feature=Feature.NANO_CONTRACTS) == FeatureState.DEFINED @@ -137,7 +142,7 @@ def test_activation(self) -> None: 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. + # At this point, the feature is not active, so the nc and fee txs are rejected on the mempool. msg = 'full validation failed: Header `NanoHeader` not supported by `Transaction`' with pytest.raises(InvalidNewTransaction, match=msg): self.vertex_handler.on_new_relayed_vertex(nc1) @@ -150,13 +155,19 @@ def test_activation(self) -> None: assert ocb1.get_metadata().validation.is_initial() assert ocb1.get_metadata().voided_by is None + msg = 'full validation failed: Header `FeeHeader` not supported by `TokenCreationTransaction`' + with pytest.raises(InvalidNewTransaction, match=msg): + self.vertex_handler.on_new_relayed_vertex(fbt) + assert fbt.get_metadata().validation.is_initial() + assert fbt.get_metadata().voided_by is None + artifacts.propagate_with(self.manager, up_to='b12') assert self.feature_service.get_state(block=b12, feature=Feature.NANO_CONTRACTS) == 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. + # Now, the nc and fee txs are accepted on the mempool. artifacts.propagate_with(self.manager, up_to='nc1') assert nc1.get_metadata().validation.is_valid() assert nc1.get_metadata().voided_by is None @@ -165,6 +176,14 @@ def test_activation(self) -> None: assert ocb1.get_metadata().validation.is_valid() assert ocb1.get_metadata().voided_by is None + artifacts.propagate_with(self.manager, up_to='FBT') + assert fbt.get_metadata().validation.is_valid() + assert fbt.get_metadata().voided_by is None + + artifacts.propagate_with(self.manager, up_to='tx1') + assert tx1.get_metadata().validation.is_valid() + assert tx1.get_metadata().voided_by is None + artifacts.propagate_with(self.manager, up_to='b13') assert nc1.get_metadata().nc_execution == NCExecutionState.SUCCESS @@ -175,24 +194,31 @@ def test_activation(self) -> None: artifacts.propagate_with(self.manager, up_to='a11') assert a11.get_metadata().validation.is_valid() assert a11.get_metadata().voided_by is None + assert b11.get_metadata().voided_by == {b11.hash} assert b12.get_metadata().voided_by == {b12.hash} assert b13.get_metadata().validation.is_invalid() + assert nc1.get_metadata().validation.is_invalid() assert ocb1.get_metadata().validation.is_invalid() - assert ocb1.get_metadata().validation.is_invalid() + assert fbt.get_metadata().validation.is_invalid() + assert tx1.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. + # The nc and fee 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) assert not self.manager.tx_storage.transaction_exists(ocb1.hash) + assert not self.manager.tx_storage.transaction_exists(fbt.hash) + assert not self.manager.tx_storage.transaction_exists(tx1.hash) assert nc1 not in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) assert ocb1 not in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) + assert fbt not in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) + assert tx1 not in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) - # The nc txs are re-accepted on the mempool. + # The nc and fee 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) == FeatureState.ACTIVE @@ -216,6 +242,20 @@ def test_activation(self) -> None: assert self.manager.tx_storage.transaction_exists(ocb1.hash) assert ocb1 in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) + fbt._metadata = None + self.vertex_handler.on_new_relayed_vertex(fbt) + assert fbt.get_metadata().validation.is_valid() + assert fbt.get_metadata().voided_by is None + assert self.manager.tx_storage.transaction_exists(fbt.hash) + assert fbt in list(self.manager.tx_storage.iter_mempool_tips_from_best_index()) + + tx1._metadata = None + self.vertex_handler.on_new_relayed_vertex(tx1) + assert tx1.get_metadata().validation.is_valid() + assert tx1.get_metadata().voided_by is None + assert self.manager.tx_storage.transaction_exists(tx1.hash) + assert tx1 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