Skip to content

Commit 73d3e56

Browse files
committed
refactor(consensus): feature activation mempool rules
1 parent ac49315 commit 73d3e56

File tree

3 files changed

+69
-57
lines changed

3 files changed

+69
-57
lines changed

hathor/builder/builder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ def _get_or_create_consensus(self) -> ConsensusAlgorithm:
423423
nc_log_storage=self._get_or_create_nc_log_storage(),
424424
nc_calls_sorter=nc_calls_sorter,
425425
feature_service=self._get_or_create_feature_service(),
426+
tx_storage=self._get_or_create_tx_storage(),
426427
)
427428

428429
return self._consensus

hathor/consensus/consensus.py

Lines changed: 67 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@
1515
from __future__ import annotations
1616

1717
from collections import defaultdict
18-
from typing import TYPE_CHECKING, Callable
18+
from typing import TYPE_CHECKING, Callable, assert_never
1919

2020
from structlog import get_logger
2121

2222
from hathor.consensus.block_consensus import BlockConsensusAlgorithmFactory
2323
from hathor.consensus.context import ConsensusAlgorithmContext
2424
from hathor.consensus.transaction_consensus import TransactionConsensusAlgorithmFactory
2525
from hathor.execution_manager import non_critical_code
26-
from hathor.feature_activation.utils import Features
26+
from hathor.feature_activation.feature import Feature
2727
from hathor.profiler import get_cpu_profiler
2828
from hathor.pubsub import HathorEvents, PubSubManager
29-
from hathor.transaction import BaseTransaction, Transaction
29+
from hathor.transaction import BaseTransaction, Block, Transaction
3030
from hathor.transaction.exceptions import RewardLocked
3131
from hathor.util import not_none
3232

@@ -78,6 +78,7 @@ def __init__(
7878
pubsub: PubSubManager,
7979
*,
8080
settings: HathorSettings,
81+
tx_storage: TransactionStorage,
8182
runner_factory: RunnerFactory,
8283
nc_calls_sorter: NCSorterCallable,
8384
nc_log_storage: NCLogStorage,
@@ -87,6 +88,7 @@ def __init__(
8788
self._settings = settings
8889
self.log = logger.new()
8990
self._pubsub = pubsub
91+
self.tx_storage = tx_storage
9092
self.nc_storage_factory = nc_storage_factory
9193
self.soft_voided_tx_ids = frozenset(soft_voided_tx_ids)
9294
self.block_algorithm_factory = BlockConsensusAlgorithmFactory(
@@ -109,8 +111,7 @@ def unsafe_update(self, base: BaseTransaction) -> None:
109111
if this method throws any exception.
110112
"""
111113
from hathor.transaction import Block, Transaction
112-
assert base.storage is not None
113-
assert base.storage.is_only_valid_allowed()
114+
assert self.tx_storage.is_only_valid_allowed()
114115
meta = base.get_metadata()
115116
assert meta.validation.is_valid()
116117

@@ -122,12 +123,10 @@ def unsafe_update(self, base: BaseTransaction) -> None:
122123
# this context instance will live only while this update is running
123124
context = self.create_context()
124125

125-
assert base.storage is not None
126-
storage = base.storage
127-
best_height, best_tip = storage.indexes.height.get_height_tip()
126+
best_height, best_tip = self.tx_storage.indexes.height.get_height_tip()
128127

129128
# This has to be called before the removal of vertices, otherwise this call may fail.
130-
old_best_block = base.storage.get_transaction(best_tip)
129+
old_best_block = self.tx_storage.get_block(best_tip)
131130

132131
if isinstance(base, Transaction):
133132
context.transaction_algorithm.update_consensus(base)
@@ -139,10 +138,10 @@ def unsafe_update(self, base: BaseTransaction) -> None:
139138
# signal a mempool tips index update for all affected transactions,
140139
# because that index is used on _compute_vertices_that_became_invalid below.
141140
for tx_affected in _sorted_affected_txs(context.txs_affected):
142-
storage.indexes.mempool_tips.update(tx_affected)
141+
self.tx_storage.indexes.mempool_tips.update(tx_affected)
143142

144143
txs_to_remove: list[BaseTransaction] = []
145-
new_best_height, new_best_tip = storage.indexes.height.get_height_tip()
144+
new_best_height, new_best_tip = self.tx_storage.indexes.height.get_height_tip()
146145

147146
if context.reorg_info is not None:
148147
if new_best_height < best_height:
@@ -152,20 +151,22 @@ def unsafe_update(self, base: BaseTransaction) -> None:
152151
)
153152

154153
# XXX: this method will mark as INVALID all transactions in the mempool that became invalid after the reorg
155-
txs_to_remove.extend(self._compute_vertices_that_became_invalid(storage, new_best_height))
154+
txs_to_remove.extend(
155+
self._compute_vertices_that_became_invalid(new_best_block=context.reorg_info.new_best_block)
156+
)
156157

157158
if txs_to_remove:
158159
self.log.warn('some transactions on the mempool became invalid and will be removed',
159160
count=len(txs_to_remove))
160161
# XXX: because transactions in `txs_to_remove` are marked as invalid, we need this context to be
161162
# able to remove them
162-
with storage.allow_invalid_context():
163-
self._remove_transactions(txs_to_remove, storage, context)
163+
with self.tx_storage.allow_invalid_context():
164+
self._remove_transactions(txs_to_remove, context)
164165

165166
# emit the reorg started event if needed
166167
if context.reorg_info is not None:
167168
assert isinstance(old_best_block, Block)
168-
new_best_block = base.storage.get_transaction(new_best_tip)
169+
new_best_block = self.tx_storage.get_transaction(new_best_tip)
169170
reorg_size = old_best_block.get_height() - context.reorg_info.common_block.get_height()
170171
# TODO: After we remove block ties, should the assert below be true?
171172
# assert old_best_block.get_metadata().voided_by
@@ -190,10 +191,9 @@ def unsafe_update(self, base: BaseTransaction) -> None:
190191

191192
# finally signal an index update for all affected transactions
192193
for tx_affected in _sorted_affected_txs(context.txs_affected):
193-
assert tx_affected.storage is not None
194-
tx_affected.storage.indexes.update_critical_indexes(tx_affected)
194+
self.tx_storage.indexes.update_critical_indexes(tx_affected)
195195
with non_critical_code(self.log):
196-
tx_affected.storage.indexes.update_non_critical_indexes(tx_affected)
196+
self.tx_storage.indexes.update_non_critical_indexes(tx_affected)
197197
context.pubsub.publish(HathorEvents.CONSENSUS_TX_UPDATE, tx=tx_affected)
198198

199199
# signal all transactions of which the execution succeeded
@@ -242,8 +242,7 @@ def _filter_out_soft_voided_entries(self, tx: BaseTransaction, voided_by: set[by
242242
continue
243243
if h in self.soft_voided_tx_ids:
244244
continue
245-
assert tx.storage is not None
246-
tx3 = tx.storage.get_transaction(h)
245+
tx3 = self.tx_storage.get_transaction(h)
247246
tx3_meta = tx3.get_metadata()
248247
tx3_voided_by: set[bytes] = tx3_meta.voided_by or set()
249248
if not (self.soft_voided_tx_ids & tx3_voided_by):
@@ -267,21 +266,15 @@ def _filter_out_nc_fail_entries(self, tx: BaseTransaction, voided_by: set[bytes]
267266
continue
268267
if h == tx.hash:
269268
continue
270-
assert tx.storage is not None
271-
tx2 = tx.storage.get_transaction(h)
269+
tx2 = self.tx_storage.get_transaction(h)
272270
tx2_meta = tx2.get_metadata()
273271
tx2_voided_by: set[bytes] = tx2_meta.voided_by or set()
274272
if NC_EXECUTION_FAIL_ID in tx2_voided_by:
275273
ret.discard(h)
276274
assert NC_EXECUTION_FAIL_ID not in ret
277275
return ret
278276

279-
def _remove_transactions(
280-
self,
281-
txs: list[BaseTransaction],
282-
storage: TransactionStorage,
283-
context: ConsensusAlgorithmContext,
284-
) -> None:
277+
def _remove_transactions(self, txs: list[BaseTransaction], context: ConsensusAlgorithmContext) -> None:
285278
"""Will remove all the transactions on the list from the database.
286279
287280
Special notes:
@@ -319,38 +312,32 @@ def _remove_transactions(
319312
spent_tx_meta.spent_outputs[tx_input.index].remove(tx.hash)
320313
context.save(spent_tx)
321314
for parent_hash, children_to_remove in parents_to_update.items():
322-
parent_tx = storage.get_transaction(parent_hash)
315+
parent_tx = self.tx_storage.get_transaction(parent_hash)
323316
for child in children_to_remove:
324-
storage.vertex_children.remove_child(parent_tx, child)
317+
self.tx_storage.vertex_children.remove_child(parent_tx, child)
325318
context.save(parent_tx)
326319
for tx in txs:
327320
self.log.debug('remove transaction', tx=tx.hash_hex)
328-
storage.remove_transaction(tx)
321+
self.tx_storage.remove_transaction(tx)
329322

330-
def _compute_vertices_that_became_invalid(
331-
self,
332-
storage: TransactionStorage,
333-
new_best_height: int,
334-
) -> list[BaseTransaction]:
323+
def _compute_vertices_that_became_invalid(self, *, new_best_block: Block) -> list[BaseTransaction]:
335324
"""This method will look for transactions in the mempool that have become invalid after a reorg."""
336325
from hathor.transaction.storage.traversal import BFSTimestampWalk
337326
from hathor.transaction.validation_state import ValidationState
338327

339-
mempool_tips = list(storage.indexes.mempool_tips.iter(storage))
328+
mempool_tips = list(self.tx_storage.indexes.mempool_tips.iter(self.tx_storage))
340329
if not mempool_tips:
341330
# Mempool is empty, nothing to remove.
342331
return []
343332

344333
mempool_rules: tuple[Callable[[Transaction], bool], ...] = (
345-
lambda tx: self._reward_lock_mempool_rule(tx, new_best_height),
346-
lambda tx: self._unknown_contract_mempool_rule(tx),
347-
lambda tx: self._nano_activation_rule(storage, tx),
348-
lambda tx: self._fee_tokens_activation_rule(storage, tx),
349-
self._checkdatasig_count_rule,
334+
lambda tx: self._reward_lock_mempool_rule(tx, new_best_block.get_height()),
335+
lambda tx: self._feature_activation_rules(tx, new_best_block),
336+
self._unknown_contract_mempool_rule,
350337
)
351338

352339
find_invalid_bfs = BFSTimestampWalk(
353-
storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=False
340+
self.tx_storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=False
354341
)
355342

356343
invalid_txs: set[BaseTransaction] = set()
@@ -373,7 +360,7 @@ def _compute_vertices_that_became_invalid(
373360
# From the invalid txs, mark all vertices to the right as invalid. This includes both txs and blocks.
374361
to_remove: list[BaseTransaction] = []
375362
find_to_remove_bfs = BFSTimestampWalk(
376-
storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=True
363+
self.tx_storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=True
377364
)
378365
for vertex in find_to_remove_bfs.run(invalid_txs, skip_root=False):
379366
vertex.set_validation(ValidationState.INVALID)
@@ -416,15 +403,40 @@ def _unknown_contract_mempool_rule(self, tx: Transaction) -> bool:
416403
return False
417404
return True
418405

419-
def _nano_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool:
406+
def _feature_activation_rules(self, tx: Transaction, new_best_block: Block) -> bool:
407+
"""Check whether a tx became invalid because of some feature state of the new best block."""
408+
features = self.feature_service.get_feature_states(vertex=new_best_block)
409+
410+
for feature, feature_state in features.items():
411+
is_active = feature_state.is_active()
412+
match feature:
413+
case Feature.NANO_CONTRACTS:
414+
if not self._nano_activation_rule(tx, is_active):
415+
return False
416+
case Feature.FEE_TOKENS:
417+
if not self._fee_tokens_activation_rule(tx, is_active):
418+
return False
419+
case Feature.COUNT_CHECKDATASIG_OP:
420+
if not self._checkdatasig_count_rule(tx):
421+
return False
422+
case (
423+
Feature.INCREASE_MAX_MERKLE_PATH_LENGTH
424+
| Feature.NOP_FEATURE_1
425+
| Feature.NOP_FEATURE_2
426+
| Feature.NOP_FEATURE_3
427+
):
428+
# These features do not affect transactions.
429+
pass
430+
case _:
431+
assert_never(feature)
432+
433+
return True
434+
435+
def _nano_activation_rule(self, tx: Transaction, is_active: bool) -> bool:
420436
"""Check whether a tx became invalid because the reorg changed the nano feature activation state."""
421437
from hathor.nanocontracts import OnChainBlueprint
422438

423-
best_block = storage.get_best_block()
424-
features = Features.from_vertex(
425-
settings=self._settings, vertex=best_block, feature_service=self.feature_service
426-
)
427-
if features.nanocontracts:
439+
if is_active:
428440
# When nano is active, this rule has no effect.
429441
return True
430442

@@ -437,18 +449,14 @@ def _nano_activation_rule(self, storage: TransactionStorage, tx: Transaction) ->
437449

438450
return True
439451

440-
def _fee_tokens_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool:
452+
def _fee_tokens_activation_rule(self, tx: Transaction, is_active: bool) -> bool:
441453
"""
442454
Check whether a tx became invalid because the reorg changed the fee-based tokens feature activation state.
443455
"""
444456
from hathor.transaction.token_creation_tx import TokenCreationTransaction
445457
from hathor.transaction.token_info import TokenVersion
446458

447-
best_block = storage.get_best_block()
448-
features = Features.from_vertex(
449-
settings=self._settings, vertex=best_block, feature_service=self.feature_service
450-
)
451-
if features.fee_tokens:
459+
if is_active:
452460
# When fee-based tokens feature is active, this rule has no effect.
453461
return True
454462

@@ -462,9 +470,11 @@ def _fee_tokens_activation_rule(self, storage: TransactionStorage, tx: Transacti
462470
return True
463471

464472
def _checkdatasig_count_rule(self, tx: Transaction) -> bool:
465-
"""Check whether a tx became invalid because the reorg changed the checkdatasig feature activation state."""
473+
"""Check whether a tx became invalid because of the count checkdatasig feature."""
466474
from hathor.verification.vertex_verifier import VertexVerifier
467475

476+
# We check all txs regardless of the feature state, because this rule
477+
# already prohibited mempool txs before the block feature activation.
468478
# Any exception in the sigops verification will be considered
469479
# a fail and the tx will be removed from the mempool.
470480
try:

hathor_cli/builder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
256256
nc_calls_sorter=nc_calls_sorter,
257257
feature_service=self.feature_service,
258258
nc_exec_fail_trace=self._args.nc_exec_fail_trace,
259+
tx_storage=tx_storage,
259260
)
260261

261262
if self._args.x_enable_event_queue or self._args.enable_event_queue:

0 commit comments

Comments
 (0)