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
41 changes: 32 additions & 9 deletions hathor/consensus/block_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from structlog import get_logger
from typing_extensions import assert_never

from hathor.consensus.context import ReorgInfo
from hathor.feature_activation.feature import Feature
from hathor.transaction import BaseTransaction, Block, Transaction
from hathor.transaction.nc_execution_state import NCExecutionState
Expand Down Expand Up @@ -103,14 +104,14 @@ def execute_nano_contracts(self, block: Block) -> None:

to_be_executed: list[Block] = []
is_reorg: bool = False
if self.context.reorg_common_block:
if self.context.reorg_info:
# handle reorgs
is_reorg = True
cur = block
# XXX We could stop when `cur_meta.nc_block_root_id is not None` but
# first we need to refactor meta.first_block and meta.voided_by to
# have different values per block.
while cur != self.context.reorg_common_block:
while cur != self.context.reorg_info.common_block:
cur_meta = cur.get_metadata()
if cur_meta.nc_block_root_id is not None:
# Reset nc_block_root_id to force re-execution.
Expand Down Expand Up @@ -190,7 +191,7 @@ def _nc_execute_calls(self, block: Block, *, is_reorg: bool) -> None:
continue
tx_meta = tx.get_metadata()
if is_reorg:
assert self.context.reorg_common_block is not None
assert self.context.reorg_info is not None
# Clear the NC_EXECUTION_FAIL_ID flag if this is the only reason the transaction was voided.
# This case might only happen when handling reorgs.
assert tx.storage is not None
Expand Down Expand Up @@ -464,7 +465,6 @@ def update_voided_info(self, block: Block) -> None:

else:
# Either everyone has the same score or there is a winner.

valid_heads = []
for head in heads:
meta = head.get_metadata()
Expand All @@ -487,19 +487,42 @@ def update_voided_info(self, block: Block) -> None:
meta = block.get_metadata()
height = block.get_height()
if not meta.voided_by:
# It is only a re-org if common_block not in heads
# This must run before updating the indexes.
if common_block not in heads:
self.mark_as_reorg_if_needed(common_block, block)
self.log.debug('index new winner block', height=height, block=block.hash_hex)
# We update the height cache index with the new winner chain
storage.indexes.height.update_new_chain(height, block)
storage.update_best_block_tips_cache([block.hash])
# It is only a re-org if common_block not in heads
if common_block not in heads:
self.context.mark_as_reorg(common_block)
else:
# This must run before updating the indexes.
meta = block.get_metadata()
if not meta.voided_by:
self.mark_as_reorg_if_needed(common_block, block)
best_block_tips = [blk.hash for blk in heads]
best_block_tips.append(block.hash)
storage.update_best_block_tips_cache(best_block_tips)
if not meta.voided_by:
self.context.mark_as_reorg(common_block)

def mark_as_reorg_if_needed(self, common_block: Block, new_best_block: Block) -> None:
"""Mark as reorg only if reorg size > 0."""
assert new_best_block.storage is not None
storage = new_best_block.storage
assert storage.indexes is not None
_, old_best_block_hash = storage.indexes.height.get_height_tip()
old_best_block = storage.get_transaction(old_best_block_hash)
assert isinstance(old_best_block, Block)

reorg_size = old_best_block.get_height() - common_block.get_height()
if reorg_size == 0:
assert old_best_block.hash == common_block.hash
return

self.context.mark_as_reorg(ReorgInfo(
common_block=common_block,
old_best_block=old_best_block,
new_best_block=new_best_block,
))

def union_voided_by_from_parents(self, block: Block) -> set[bytes]:
"""Return the union of the voided_by of block's parents.
Expand Down
21 changes: 13 additions & 8 deletions hathor/consensus/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def unsafe_update(self, base: BaseTransaction) -> None:
txs_to_remove: list[BaseTransaction] = []
new_best_height, new_best_tip = storage.indexes.height.get_height_tip()

if context.reorg_common_block is not None:
if context.reorg_info is not None:
if new_best_height < best_height:
self.log.warn(
'height decreased, re-checking mempool', prev_height=best_height, new_height=new_best_height,
Expand All @@ -162,16 +162,21 @@ def unsafe_update(self, base: BaseTransaction) -> None:
self._remove_transactions(txs_to_remove, storage, context)

# emit the reorg started event if needed
if context.reorg_common_block is not None:
if context.reorg_info is not None:
assert isinstance(old_best_block, Block)
new_best_block = base.storage.get_transaction(new_best_tip)
reorg_size = old_best_block.get_height() - context.reorg_common_block.get_height()
reorg_size = old_best_block.get_height() - context.reorg_info.common_block.get_height()
assert old_best_block != new_best_block
assert reorg_size > 0
context.pubsub.publish(HathorEvents.REORG_STARTED, old_best_height=best_height,
old_best_block=old_best_block, new_best_height=new_best_height,
new_best_block=new_best_block, common_block=context.reorg_common_block,
reorg_size=reorg_size)
context.pubsub.publish(
HathorEvents.REORG_STARTED,
old_best_height=best_height,
old_best_block=old_best_block,
new_best_height=new_best_height,
new_best_block=new_best_block,
common_block=context.reorg_info.common_block,
reorg_size=reorg_size,
)

# finally signal an index update for all affected transactions
for tx_affected in _sorted_affected_txs(context.txs_affected):
Expand All @@ -195,7 +200,7 @@ def unsafe_update(self, base: BaseTransaction) -> None:
context.pubsub.publish(HathorEvents.CONSENSUS_TX_REMOVED, tx=tx_removed)

# and also emit the reorg finished event if needed
if context.reorg_common_block is not None:
if context.reorg_info is not None:
context.pubsub.publish(HathorEvents.REORG_FINISHED)

def filter_out_voided_by_entries_from_parents(self, tx: BaseTransaction, voided_by: set[bytes]) -> set[bytes]:
Expand Down
29 changes: 23 additions & 6 deletions hathor/consensus/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Optional
from dataclasses import dataclass
from typing import TYPE_CHECKING

from structlog import get_logger

Expand All @@ -32,16 +33,32 @@
_base_transaction_log = logger.new()


@dataclass(kw_only=True, slots=True, frozen=True)
class ReorgInfo:
common_block: Block
old_best_block: Block
new_best_block: Block


class ConsensusAlgorithmContext:
""" An instance of this class holds all the relevant information related to a single run of a consensus update.
"""
__slots__ = (
'consensus',
'pubsub',
'block_algorithm',
'transaction_algorithm',
'txs_affected',
'reorg_info',
'nc_events',
)

consensus: 'ConsensusAlgorithm'
pubsub: PubSubManager
block_algorithm: 'BlockConsensusAlgorithm'
transaction_algorithm: 'TransactionConsensusAlgorithm'
txs_affected: set[BaseTransaction]
reorg_common_block: Optional[Block]
reorg_info: ReorgInfo | None
nc_events: list[tuple[Transaction, list[NCEvent]]] | None

def __init__(self, consensus: 'ConsensusAlgorithm', pubsub: PubSubManager) -> None:
Expand All @@ -50,7 +67,7 @@ def __init__(self, consensus: 'ConsensusAlgorithm', pubsub: PubSubManager) -> No
self.block_algorithm = self.consensus.block_algorithm_factory(self)
self.transaction_algorithm = self.consensus.transaction_algorithm_factory(self)
self.txs_affected = set()
self.reorg_common_block = None
self.reorg_info = None
self.nc_events = None

def save(self, tx: BaseTransaction) -> None:
Expand All @@ -59,7 +76,7 @@ def save(self, tx: BaseTransaction) -> None:
self.txs_affected.add(tx)
tx.storage.save_transaction(tx, only_metadata=True)

def mark_as_reorg(self, common_block: Block) -> None:
def mark_as_reorg(self, reorg_info: ReorgInfo) -> None:
"""Must only be called once, will raise an assert error if called twice."""
assert self.reorg_common_block is None
self.reorg_common_block = common_block
assert self.reorg_info is None
self.reorg_info = reorg_info
Loading