1515from __future__ import annotations
1616
1717from collections import defaultdict
18- from typing import TYPE_CHECKING , Callable
18+ from typing import TYPE_CHECKING , Callable , assert_never
1919
2020from structlog import get_logger
2121
2222from hathor .consensus .block_consensus import BlockConsensusAlgorithmFactory
2323from hathor .consensus .context import ConsensusAlgorithmContext
2424from hathor .consensus .transaction_consensus import TransactionConsensusAlgorithmFactory
2525from 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
2728from hathor .profiler import get_cpu_profiler
2829from hathor .pubsub import HathorEvents , PubSubManager
29- from hathor .transaction import BaseTransaction , Transaction
30+ from hathor .transaction import BaseTransaction , Block , Transaction
3031from hathor .transaction .exceptions import RewardLocked
3132from 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
477513def _sorted_affected_txs (affected_txs : set [BaseTransaction ]) -> list [BaseTransaction ]:
478514 """
0 commit comments