Skip to content

Commit 076215e

Browse files
committed
refactor(consensus): feature activation mempool rules
1 parent 8bcd974 commit 076215e

File tree

3 files changed

+91
-53
lines changed

3 files changed

+91
-53
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: 89 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@
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
27+
from hathor.feature_activation.model.feature_state import FeatureState
2728
from hathor.profiler import get_cpu_profiler
2829
from hathor.pubsub import HathorEvents, PubSubManager
29-
from hathor.transaction import BaseTransaction, Transaction
30+
from hathor.transaction import BaseTransaction, Block, Transaction
3031
from hathor.transaction.exceptions import RewardLocked
3132
from hathor.util import not_none
3233

@@ -78,6 +79,7 @@ def __init__(
7879
pubsub: PubSubManager,
7980
*,
8081
settings: HathorSettings,
82+
tx_storage: TransactionStorage,
8183
runner_factory: RunnerFactory,
8284
nc_calls_sorter: NCSorterCallable,
8385
nc_log_storage: NCLogStorage,
@@ -87,6 +89,7 @@ def __init__(
8789
self._settings = settings
8890
self.log = logger.new()
8991
self._pubsub = pubsub
92+
self.tx_storage = tx_storage
9093
self.nc_storage_factory = nc_storage_factory
9194
self.soft_voided_tx_ids = frozenset(soft_voided_tx_ids)
9295
self.block_algorithm_factory = BlockConsensusAlgorithmFactory(
@@ -109,8 +112,7 @@ def unsafe_update(self, base: BaseTransaction) -> None:
109112
if this method throws any exception.
110113
"""
111114
from hathor.transaction import Block, Transaction
112-
assert base.storage is not None
113-
assert base.storage.is_only_valid_allowed()
115+
assert self.tx_storage.is_only_valid_allowed()
114116
meta = base.get_metadata()
115117
assert meta.validation.is_valid()
116118

@@ -122,12 +124,10 @@ def unsafe_update(self, base: BaseTransaction) -> None:
122124
# this context instance will live only while this update is running
123125
context = self.create_context()
124126

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

129129
# 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)
130+
old_best_block = self.tx_storage.get_block(best_tip)
131131

132132
if isinstance(base, Transaction):
133133
context.transaction_algorithm.update_consensus(base)
@@ -139,10 +139,10 @@ def unsafe_update(self, base: BaseTransaction) -> None:
139139
# signal a mempool tips index update for all affected transactions,
140140
# because that index is used on _compute_vertices_that_became_invalid below.
141141
for tx_affected in _sorted_affected_txs(context.txs_affected):
142-
storage.indexes.mempool_tips.update(tx_affected)
142+
self.tx_storage.indexes.mempool_tips.update(tx_affected)
143143

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

147147
if context.reorg_info is not None:
148148
if new_best_height < best_height:
@@ -152,20 +152,25 @@ def unsafe_update(self, base: BaseTransaction) -> None:
152152
)
153153

154154
# 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))
155+
txs_to_remove.extend(
156+
self._compute_vertices_that_became_invalid(
157+
old_best_block=old_best_block,
158+
new_best_block=context.reorg_info.new_best_block,
159+
)
160+
)
156161

157162
if txs_to_remove:
158163
self.log.warn('some transactions on the mempool became invalid and will be removed',
159164
count=len(txs_to_remove))
160165
# XXX: because transactions in `txs_to_remove` are marked as invalid, we need this context to be
161166
# able to remove them
162-
with storage.allow_invalid_context():
163-
self._remove_transactions(txs_to_remove, storage, context)
167+
with self.tx_storage.allow_invalid_context():
168+
self._remove_transactions(txs_to_remove, context)
164169

165170
# emit the reorg started event if needed
166171
if context.reorg_info is not None:
167172
assert isinstance(old_best_block, Block)
168-
new_best_block = base.storage.get_transaction(new_best_tip)
173+
new_best_block = self.tx_storage.get_transaction(new_best_tip)
169174
reorg_size = old_best_block.get_height() - context.reorg_info.common_block.get_height()
170175
# TODO: After we remove block ties, should the assert below be true?
171176
# assert old_best_block.get_metadata().voided_by
@@ -190,10 +195,9 @@ def unsafe_update(self, base: BaseTransaction) -> None:
190195

191196
# finally signal an index update for all affected transactions
192197
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)
198+
self.tx_storage.indexes.update_critical_indexes(tx_affected)
195199
with non_critical_code(self.log):
196-
tx_affected.storage.indexes.update_non_critical_indexes(tx_affected)
200+
self.tx_storage.indexes.update_non_critical_indexes(tx_affected)
197201
context.pubsub.publish(HathorEvents.CONSENSUS_TX_UPDATE, tx=tx_affected)
198202

199203
# signal all transactions of which the execution succeeded
@@ -242,8 +246,7 @@ def _filter_out_soft_voided_entries(self, tx: BaseTransaction, voided_by: set[by
242246
continue
243247
if h in self.soft_voided_tx_ids:
244248
continue
245-
assert tx.storage is not None
246-
tx3 = tx.storage.get_transaction(h)
249+
tx3 = self.tx_storage.get_transaction(h)
247250
tx3_meta = tx3.get_metadata()
248251
tx3_voided_by: set[bytes] = tx3_meta.voided_by or set()
249252
if not (self.soft_voided_tx_ids & tx3_voided_by):
@@ -267,21 +270,15 @@ def _filter_out_nc_fail_entries(self, tx: BaseTransaction, voided_by: set[bytes]
267270
continue
268271
if h == tx.hash:
269272
continue
270-
assert tx.storage is not None
271-
tx2 = tx.storage.get_transaction(h)
273+
tx2 = self.tx_storage.get_transaction(h)
272274
tx2_meta = tx2.get_metadata()
273275
tx2_voided_by: set[bytes] = tx2_meta.voided_by or set()
274276
if NC_EXECUTION_FAIL_ID in tx2_voided_by:
275277
ret.discard(h)
276278
assert NC_EXECUTION_FAIL_ID not in ret
277279
return ret
278280

279-
def _remove_transactions(
280-
self,
281-
txs: list[BaseTransaction],
282-
storage: TransactionStorage,
283-
context: ConsensusAlgorithmContext,
284-
) -> None:
281+
def _remove_transactions(self, txs: list[BaseTransaction], context: ConsensusAlgorithmContext) -> None:
285282
"""Will remove all the transactions on the list from the database.
286283
287284
Special notes:
@@ -319,38 +316,37 @@ def _remove_transactions(
319316
spent_tx_meta.spent_outputs[tx_input.index].remove(tx.hash)
320317
context.save(spent_tx)
321318
for parent_hash, children_to_remove in parents_to_update.items():
322-
parent_tx = storage.get_transaction(parent_hash)
319+
parent_tx = self.tx_storage.get_transaction(parent_hash)
323320
for child in children_to_remove:
324-
storage.vertex_children.remove_child(parent_tx, child)
321+
self.tx_storage.vertex_children.remove_child(parent_tx, child)
325322
context.save(parent_tx)
326323
for tx in txs:
327324
self.log.debug('remove transaction', tx=tx.hash_hex)
328-
storage.remove_transaction(tx)
325+
self.tx_storage.remove_transaction(tx)
329326

330327
def _compute_vertices_that_became_invalid(
331328
self,
332-
storage: TransactionStorage,
333-
new_best_height: int,
329+
*,
330+
old_best_block: Block,
331+
new_best_block: Block,
334332
) -> list[BaseTransaction]:
335333
"""This method will look for transactions in the mempool that have become invalid after a reorg."""
336334
from hathor.transaction.storage.traversal import BFSTimestampWalk
337335
from hathor.transaction.validation_state import ValidationState
338336

339-
mempool_tips = list(storage.indexes.mempool_tips.iter(storage))
337+
mempool_tips = list(self.tx_storage.indexes.mempool_tips.iter(self.tx_storage))
340338
if not mempool_tips:
341339
# Mempool is empty, nothing to remove.
342340
return []
343341

344342
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,
343+
lambda tx: self._reward_lock_mempool_rule(tx, new_best_block.get_height()),
344+
lambda tx: self._feature_activation_rules(tx, old_best_block, new_best_block),
345+
self._unknown_contract_mempool_rule,
350346
)
351347

352348
find_invalid_bfs = BFSTimestampWalk(
353-
storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=False
349+
self.tx_storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=False
354350
)
355351

356352
invalid_txs: set[BaseTransaction] = set()
@@ -373,7 +369,7 @@ def _compute_vertices_that_became_invalid(
373369
# From the invalid txs, mark all vertices to the right as invalid. This includes both txs and blocks.
374370
to_remove: list[BaseTransaction] = []
375371
find_to_remove_bfs = BFSTimestampWalk(
376-
storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=True
372+
self.tx_storage, is_dag_funds=True, is_dag_verifications=True, is_left_to_right=True
377373
)
378374
for vertex in find_to_remove_bfs.run(invalid_txs, skip_root=False):
379375
vertex.set_validation(ValidationState.INVALID)
@@ -416,15 +412,39 @@ def _unknown_contract_mempool_rule(self, tx: Transaction) -> bool:
416412
return False
417413
return True
418414

419-
def _nano_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool:
415+
def _feature_activation_rules(self, tx: Transaction, old_best_block: Block, new_best_block: Block) -> bool:
416+
"""Check whether a tx became invalid because the reorg flipped the feature activation state of some feature."""
417+
flipped = self._feature_activation_flips(old_best_block=old_best_block, new_best_block=new_best_block)
418+
419+
for feature, is_active in flipped.items():
420+
match feature:
421+
case Feature.NANO_CONTRACTS:
422+
if not self._nano_activation_rule(tx, is_active):
423+
return False
424+
case Feature.FEE_TOKENS:
425+
if not self._fee_tokens_activation_rule(tx, is_active):
426+
return False
427+
case Feature.COUNT_CHECKDATASIG_OP:
428+
if not self._checkdatasig_count_rule(tx):
429+
return False
430+
case (
431+
Feature.INCREASE_MAX_MERKLE_PATH_LENGTH
432+
| Feature.NOP_FEATURE_1
433+
| Feature.NOP_FEATURE_2
434+
| Feature.NOP_FEATURE_3
435+
):
436+
# These features do not affect transactions.
437+
pass
438+
case _:
439+
assert_never(feature)
440+
441+
return True
442+
443+
def _nano_activation_rule(self, tx: Transaction, is_active: bool) -> bool:
420444
"""Check whether a tx became invalid because the reorg changed the nano feature activation state."""
421445
from hathor.nanocontracts import OnChainBlueprint
422446

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.nano:
447+
if is_active:
428448
# When nano is active, this rule has no effect.
429449
return True
430450

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

438458
return True
439459

440-
def _fee_tokens_activation_rule(self, storage: TransactionStorage, tx: Transaction) -> bool:
460+
def _fee_tokens_activation_rule(self, tx: Transaction, is_active: bool) -> bool:
441461
"""
442462
Check whether a tx became invalid because the reorg changed the fee-based tokens feature activation state.
443463
"""
444464
from hathor.transaction.token_creation_tx import TokenCreationTransaction
445465
from hathor.transaction.token_info import TokenVersion
446466

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:
467+
if is_active:
452468
# When fee-based tokens feature is active, this rule has no effect.
453469
return True
454470

@@ -473,6 +489,26 @@ def _checkdatasig_count_rule(self, tx: Transaction) -> bool:
473489
return False
474490
return True
475491

492+
def _feature_activation_flips(self, *, old_best_block: Block, new_best_block: Block) -> dict[Feature, bool]:
493+
"""
494+
Get a diff of feature activation states that flipped from active to non-active,
495+
or vice-verse, between the old best block and the new best block.
496+
Returns a dict with each feature that was flipped, and its new state.
497+
"""
498+
old_feature_states = self.feature_service.get_feature_states(vertex=old_best_block)
499+
new_feature_states = self.feature_service.get_feature_states(vertex=new_best_block)
500+
all_features: set[Feature] = {*old_feature_states.keys(), *new_feature_states.keys()}
501+
flipped: dict[Feature, bool] = {}
502+
503+
for feature in all_features:
504+
old_state = old_feature_states.get(feature, FeatureState.DEFINED).is_active()
505+
new_state = new_feature_states.get(feature, FeatureState.DEFINED).is_active()
506+
507+
if old_state != new_state:
508+
flipped[feature] = new_state
509+
510+
return flipped
511+
476512

477513
def _sorted_affected_txs(affected_txs: set[BaseTransaction]) -> list[BaseTransaction]:
478514
"""

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)