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
48 changes: 48 additions & 0 deletions hathor/cli/events_simulator/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ class Scenario(Enum):
SINGLE_CHAIN_ONE_BLOCK = 'SINGLE_CHAIN_ONE_BLOCK'
SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS = 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS'
REORG = 'REORG'
UNVOIDED_TRANSACTION = 'UNVOIDED_TRANSACTION'

def simulate(self, simulator: 'Simulator', manager: 'HathorManager') -> None:
simulate_fns = {
Scenario.ONLY_LOAD: simulate_only_load,
Scenario.SINGLE_CHAIN_ONE_BLOCK: simulate_single_chain_one_block,
Scenario.SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS: simulate_single_chain_blocks_and_transactions,
Scenario.REORG: simulate_reorg,
Scenario.UNVOIDED_TRANSACTION: simulate_unvoided_transaction,
}

simulate_fn = simulate_fns[self]
Expand Down Expand Up @@ -92,3 +94,49 @@ def simulate_reorg(simulator: 'Simulator', manager: 'HathorManager') -> None:
connection = FakeConnection(manager, manager2)
simulator.add_connection(connection)
simulator.run(60)


def simulate_unvoided_transaction(simulator: 'Simulator', manager: 'HathorManager') -> None:
from hathor.conf.get_settings import get_settings
from hathor.simulator.utils import add_new_block, add_new_blocks, gen_new_tx
from hathor.util import not_none

settings = get_settings()
assert manager.wallet is not None
address = manager.wallet.get_unused_address(mark_as_used=False)

add_new_blocks(manager, settings.REWARD_SPEND_MIN_BLOCKS + 1)
simulator.run(60)

# A tx is created with weight 19.0005
tx = gen_new_tx(manager, address, 1000)
tx.weight = 19.0005
tx.update_hash()
assert manager.propagate_tx(tx, fails_silently=False)
simulator.run(60)

# A clone is created with a greater timestamp and a lower weight. It's a voided twin tx.
tx2 = tx.clone(include_metadata=False)
tx2.timestamp += 60
tx2.weight = 19
tx2.update_hash()
assert manager.propagate_tx(tx2, fails_silently=False)
simulator.run(60)

# Only the second tx is voided
assert not tx.get_metadata().voided_by
assert tx2.get_metadata().voided_by

# We add a block confirming the second tx, increasing its acc weight
block = add_new_block(manager, propagate=False)
block.parents = [
block.parents[0],
settings.GENESIS_TX1_HASH,
not_none(tx2.hash),
]
assert manager.propagate_tx(block, fails_silently=False)
simulator.run(60)

# The first tx gets voided and the second gets unvoided
assert tx.get_metadata().voided_by
assert not tx2.get_metadata().voided_by
17 changes: 15 additions & 2 deletions hathor/consensus/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ def _unsafe_update(self, base: BaseTransaction) -> None:
reorg_size=reorg_size)

# finally signal an index update for all affected transactions
sorted_txs_affected = sorted(context.txs_affected, key=lambda tx: not_none(tx.timestamp), reverse=True)
for tx_affected in sorted_txs_affected:
for tx_affected in _sorted_affected_txs(context.txs_affected):
assert tx_affected.storage is not None
assert tx_affected.storage.indexes is not None
tx_affected.storage.indexes.update(tx_affected)
Expand Down Expand Up @@ -167,3 +166,17 @@ def filter_out_soft_voided_entries(self, tx: BaseTransaction, voided_by: set[byt
if not (self.soft_voided_tx_ids & tx3_voided_by):
ret.add(h)
return ret


def _sorted_affected_txs(affected_txs: set[BaseTransaction]) -> list[BaseTransaction]:
"""
Sort affected txs by voided first, then descending timestamp (reverse topological order).
This is useful for generating Reliable Integration events.
"""
def sorter(tx: BaseTransaction) -> tuple[bool, int]:
meta = tx.get_metadata()
is_voided = bool(meta.voided_by)

return is_voided, not_none(tx.timestamp)

return sorted(affected_txs, key=sorter, reverse=True)
4 changes: 2 additions & 2 deletions hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,13 +825,13 @@ def serialize_output(tx: BaseTransaction, tx_out: TxOutput) -> dict[str, Any]:

return ret

def clone(self) -> 'BaseTransaction':
def clone(self, *, include_metadata: bool = True) -> 'BaseTransaction':
"""Return exact copy without sharing memory, including metadata if loaded.

:return: Transaction or Block copy
"""
new_tx = self.create_from_struct(self.get_struct())
if hasattr(self, '_metadata'):
if hasattr(self, '_metadata') and include_metadata:
assert self._metadata is not None # FIXME: is this actually true or do we have to check if not None
new_tx._metadata = self._metadata.clone()
new_tx.storage = self.storage
Expand Down
2 changes: 1 addition & 1 deletion tests/event/test_event_reorg.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ def test_reorg_events(self):
(EventType.NEW_VERTEX_ACCEPTED, {'hash': blocks[9].hash_hex}),
(EventType.REORG_STARTED, {'reorg_size': 2, 'previous_best_block': blocks[9].hash_hex,
'new_best_block': b0.hash_hex}),
(EventType.VERTEX_METADATA_CHANGED, {'hash': b0.hash_hex}),
(EventType.VERTEX_METADATA_CHANGED, {'hash': blocks[9].hash_hex}),
(EventType.VERTEX_METADATA_CHANGED, {'hash': blocks[8].hash_hex}),
(EventType.VERTEX_METADATA_CHANGED, {'hash': b0.hash_hex}),
(EventType.REORG_FINISHED, {}),
(EventType.NEW_VERTEX_ACCEPTED, {'hash': b0.hash_hex}),
]
Expand Down
Loading