Skip to content

Commit 59bdf45

Browse files
committed
feat(reliable-integration): change ordering to send voided txs first
1 parent 7b6eb34 commit 59bdf45

File tree

5 files changed

+134
-6
lines changed

5 files changed

+134
-6
lines changed

hathor/cli/events_simulator/scenario.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ class Scenario(Enum):
2525
SINGLE_CHAIN_ONE_BLOCK = 'SINGLE_CHAIN_ONE_BLOCK'
2626
SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS = 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS'
2727
REORG = 'REORG'
28+
UNVOIDED_TRANSACTION = 'UNVOIDED_TRANSACTION'
2829

2930
def simulate(self, simulator: 'Simulator', manager: 'HathorManager') -> None:
3031
simulate_fns = {
3132
Scenario.ONLY_LOAD: simulate_only_load,
3233
Scenario.SINGLE_CHAIN_ONE_BLOCK: simulate_single_chain_one_block,
3334
Scenario.SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS: simulate_single_chain_blocks_and_transactions,
3435
Scenario.REORG: simulate_reorg,
36+
Scenario.UNVOIDED_TRANSACTION: simulate_unvoided_transaction,
3537
}
3638

3739
simulate_fn = simulate_fns[self]
@@ -92,3 +94,49 @@ def simulate_reorg(simulator: 'Simulator', manager: 'HathorManager') -> None:
9294
connection = FakeConnection(manager, manager2)
9395
simulator.add_connection(connection)
9496
simulator.run(60)
97+
98+
99+
def simulate_unvoided_transaction(simulator: 'Simulator', manager: 'HathorManager') -> None:
100+
from hathor.conf.get_settings import get_settings
101+
from hathor.simulator.utils import add_new_block, add_new_blocks, gen_new_tx
102+
from hathor.util import not_none
103+
104+
settings = get_settings()
105+
assert manager.wallet is not None
106+
address = manager.wallet.get_unused_address(mark_as_used=False)
107+
108+
add_new_blocks(manager, settings.REWARD_SPEND_MIN_BLOCKS + 1)
109+
simulator.run(60)
110+
111+
# A tx is created with weight 19.0005
112+
tx = gen_new_tx(manager, address, 1000)
113+
tx.weight = 19.0005
114+
tx.update_hash()
115+
assert manager.propagate_tx(tx, fails_silently=False)
116+
simulator.run(60)
117+
118+
# A clone is created with a greater timestamp and a lower weight. It's a voided twin tx.
119+
tx2 = tx.clone(include_metadata=False)
120+
tx2.timestamp += 60
121+
tx2.weight = 19
122+
tx2.update_hash()
123+
assert manager.propagate_tx(tx2, fails_silently=False)
124+
simulator.run(60)
125+
126+
# Only the second tx is voided
127+
assert not tx.get_metadata().voided_by
128+
assert tx2.get_metadata().voided_by
129+
130+
# We add a block confirming the second tx, increasing its acc weight
131+
block = add_new_block(manager, propagate=False)
132+
block.parents = [
133+
block.parents[0],
134+
settings.GENESIS_TX1_HASH,
135+
not_none(tx2.hash),
136+
]
137+
assert manager.propagate_tx(block, fails_silently=False)
138+
simulator.run(60)
139+
140+
# The first tx gets voided and the second gets unvoided
141+
assert tx.get_metadata().voided_by
142+
assert not tx2.get_metadata().voided_by

hathor/consensus/consensus.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,7 @@ def _unsafe_update(self, base: BaseTransaction) -> None:
136136
reorg_size=reorg_size)
137137

138138
# finally signal an index update for all affected transactions
139-
sorted_txs_affected = sorted(context.txs_affected, key=lambda tx: not_none(tx.timestamp), reverse=True)
140-
for tx_affected in sorted_txs_affected:
139+
for tx_affected in _sorted_affected_txs(context.txs_affected):
141140
assert tx_affected.storage is not None
142141
assert tx_affected.storage.indexes is not None
143142
tx_affected.storage.indexes.update(tx_affected)
@@ -167,3 +166,17 @@ def filter_out_soft_voided_entries(self, tx: BaseTransaction, voided_by: set[byt
167166
if not (self.soft_voided_tx_ids & tx3_voided_by):
168167
ret.add(h)
169168
return ret
169+
170+
171+
def _sorted_affected_txs(affected_txs: set[BaseTransaction]) -> list[BaseTransaction]:
172+
"""
173+
Sort affected txs by voided first, then descending timestamp (reverse topological order).
174+
This is useful for generating Reliable Integration events.
175+
"""
176+
def sorter(tx: BaseTransaction) -> tuple[bool, int]:
177+
meta = tx.get_metadata()
178+
is_voided = bool(meta.voided_by)
179+
180+
return is_voided, not_none(tx.timestamp)
181+
182+
return sorted(affected_txs, key=sorter, reverse=True)

hathor/transaction/base_transaction.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,13 +825,13 @@ def serialize_output(tx: BaseTransaction, tx_out: TxOutput) -> dict[str, Any]:
825825

826826
return ret
827827

828-
def clone(self) -> 'BaseTransaction':
828+
def clone(self, *, include_metadata: bool = True) -> 'BaseTransaction':
829829
"""Return exact copy without sharing memory, including metadata if loaded.
830830
831831
:return: Transaction or Block copy
832832
"""
833833
new_tx = self.create_from_struct(self.get_struct())
834-
if hasattr(self, '_metadata'):
834+
if hasattr(self, '_metadata') and include_metadata:
835835
assert self._metadata is not None # FIXME: is this actually true or do we have to check if not None
836836
new_tx._metadata = self._metadata.clone()
837837
new_tx.storage = self.storage

tests/event/test_event_reorg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def test_reorg_events(self):
7777
(EventType.NEW_VERTEX_ACCEPTED, {'hash': blocks[9].hash_hex}),
7878
(EventType.REORG_STARTED, {'reorg_size': 2, 'previous_best_block': blocks[9].hash_hex,
7979
'new_best_block': b0.hash_hex}),
80-
(EventType.VERTEX_METADATA_CHANGED, {'hash': b0.hash_hex}),
8180
(EventType.VERTEX_METADATA_CHANGED, {'hash': blocks[9].hash_hex}),
8281
(EventType.VERTEX_METADATA_CHANGED, {'hash': blocks[8].hash_hex}),
82+
(EventType.VERTEX_METADATA_CHANGED, {'hash': b0.hash_hex}),
8383
(EventType.REORG_FINISHED, {}),
8484
(EventType.NEW_VERTEX_ACCEPTED, {'hash': b0.hash_hex}),
8585
]

0 commit comments

Comments
 (0)