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
14 changes: 13 additions & 1 deletion hathor/conf/mainnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
}
)
)
33 changes: 31 additions & 2 deletions hathor/consensus/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]:
Expand Down
18 changes: 16 additions & 2 deletions hathor/verification/vertex_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))

Expand Down
58 changes: 49 additions & 9 deletions tests/nanocontracts/test_nano_feature_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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
Expand Down
Loading