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
2 changes: 1 addition & 1 deletion hathor/_openapi/openapi_base.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"info": {
"title": "Hathor API",
"version": "0.68.1"
"version": "0.68.2"
},
"consumes": [
"application/json"
Expand Down
128 changes: 51 additions & 77 deletions hathor/consensus/block_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import hashlib
import traceback
from itertools import chain
from typing import TYPE_CHECKING, Any, Iterable, Optional, cast
from typing import TYPE_CHECKING, Any, Iterable, Optional

from structlog import get_logger
from typing_extensions import assert_never
Expand Down Expand Up @@ -465,10 +465,6 @@ def update_voided_info(self, block: Block) -> None:
meta = block.get_metadata()
if not meta.voided_by:
storage.indexes.height.add_new(block.get_height(), block.hash, block.timestamp)
storage.update_best_block_tips_cache([block.hash])
# The following assert must be true, but it is commented out for performance reasons.
if self._settings.SLOW_ASSERTS:
assert len(storage.get_best_block_tips(skip_cache=True)) == 1
else:
# Resolve all other cases, but (i).
log = self.log.new(block=block.hash_hex)
Expand All @@ -481,66 +477,54 @@ def update_voided_info(self, block: Block) -> None:
self.mark_as_voided(block, skip_remove_first_block_markers=True)

# Get the score of the best chains.
heads = [cast(Block, storage.get_transaction(h)) for h in storage.get_best_block_tips()]
best_score: int | None = None
for head in heads:
head_meta = head.get_metadata(force_reload=True)
if best_score is None:
best_score = head_meta.score
else:
# All heads must have the same score.
assert best_score == head_meta.score
assert best_score is not None
head = storage.get_best_block()
head_meta = head.get_metadata(force_reload=True)
best_score = head_meta.score

# Calculate the score.
# We cannot calculate score before getting the heads.
score = self.calculate_score(block)

# Finally, check who the winner is.
if score < best_score:
# Just update voided_by from parents.
winner = False

if score > best_score:
winner = True
elif score == best_score:
# Use block hashes as a tie breaker.
if block.hash < head.hash:
winner = True

if head_meta.voided_by:
# The head cannot be stale. But the current block conflict resolution has already been
# resolved and it might void the head. If this happened, it means that block has a greater
# score so we just assert it.
assert score > best_score
assert winner

if not winner:
# Not enough score, just update voided_by from parents.
self.update_voided_by_from_parents(block)

else:
# Either everyone has the same score or there is a winner.
valid_heads = []
for head in heads:
meta = head.get_metadata()
if not meta.voided_by:
valid_heads.append(head)

# We must have at most one valid head.
# Either we have a single best chain or all chains have already been voided.
assert len(valid_heads) <= 1, 'We must never have more than one valid head'

# Winner, winner, chicken dinner!
# Add voided_by to all heads.
common_block = self._find_first_parent_in_best_chain(block)
self.add_voided_by_to_multiple_chains(block, heads, common_block)

if score > best_score:
# We have a new winner candidate.
self.update_score_and_mark_as_the_best_chain_if_possible(block)
# As `update_score_and_mark_as_the_best_chain_if_possible` may affect `voided_by`,
# we need to check that block is not voided.
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])
else:
self.add_voided_by_to_multiple_chains([head], common_block)

# We have a new winner candidate.
self.update_score_and_mark_as_the_best_chain_if_possible(block)
# As `update_score_and_mark_as_the_best_chain_if_possible` may affect `voided_by`,
# we need to check that block is not voided.
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.
meta = block.get_metadata()
if not meta.voided_by:
if common_block != head:
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)
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)

def mark_as_reorg_if_needed(self, common_block: Block, new_best_block: Block) -> None:
"""Mark as reorg only if reorg size > 0."""
Expand Down Expand Up @@ -603,7 +587,7 @@ def update_voided_by_from_parents(self, block: Block) -> bool:
return True
return False

def add_voided_by_to_multiple_chains(self, block: Block, heads: list[Block], first_block: Block) -> None:
def add_voided_by_to_multiple_chains(self, heads: list[Block], first_block: Block) -> None:
# We need to go through all side chains because there may be non-voided blocks
# that must be voided.
# For instance, imagine two chains with intersection with both heads voided.
Expand All @@ -630,31 +614,13 @@ def update_score_and_mark_as_the_best_chain_if_possible(self, block: Block) -> N
self.update_score_and_mark_as_the_best_chain(block)
self.remove_voided_by_from_chain(block)

best_score: int
if self.update_voided_by_from_parents(block):
storage = block.storage
heads = [cast(Block, storage.get_transaction(h)) for h in storage.get_best_block_tips()]
best_score = 0
best_heads: list[Block]
for head in heads:
head_meta = head.get_metadata(force_reload=True)
if head_meta.score < best_score:
continue

if head_meta.score > best_score:
best_heads = [head]
best_score = head_meta.score
else:
assert best_score == head_meta.score
best_heads.append(head)
assert isinstance(best_score, int) and best_score > 0

assert len(best_heads) > 0
first_block = self._find_first_parent_in_best_chain(best_heads[0])
self.add_voided_by_to_multiple_chains(best_heads[0], [block], first_block)
if len(best_heads) == 1:
assert best_heads[0].hash != block.hash
self.update_score_and_mark_as_the_best_chain_if_possible(best_heads[0])
head = storage.get_best_block()
first_block = self._find_first_parent_in_best_chain(head)
self.add_voided_by_to_multiple_chains([block], first_block)
if head.hash != block.hash:
self.update_score_and_mark_as_the_best_chain_if_possible(head)

def update_score_and_mark_as_the_best_chain(self, block: Block) -> None:
""" Update score and mark the chain as the best chain.
Expand Down Expand Up @@ -772,6 +738,8 @@ def remove_voided_by(self, block: Block, voided_hash: Optional[bytes] = None) ->
def remove_first_block_markers(self, block: Block) -> None:
""" Remove all `meta.first_block` pointing to this block.
"""
from hathor.nanocontracts import NC_EXECUTION_FAIL_ID

assert block.storage is not None
storage = block.storage

Expand All @@ -794,6 +762,12 @@ def remove_first_block_markers(self, block: Block) -> None:
tx.storage.indexes.handle_contract_unexecution(tx)
meta.nc_execution = NCExecutionState.PENDING
meta.nc_calls = None
meta.nc_events = None
if meta.voided_by == {tx.hash, NC_EXECUTION_FAIL_ID}:
assert isinstance(tx, Transaction)
self.context.transaction_algorithm.remove_voided_by(tx, tx.hash)
assert meta.voided_by == {NC_EXECUTION_FAIL_ID}
meta.voided_by = None
meta.first_block = None
self.context.save(tx)

Expand Down
1 change: 0 additions & 1 deletion hathor/consensus/transaction_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,6 @@ def add_voided_by(self, tx: Transaction, voided_hash: bytes, *, is_dag_verificat
if tx2.is_block:
assert isinstance(tx2, Block)
self.context.block_algorithm.mark_as_voided(tx2)
tx2.storage.update_best_block_tips_cache(None)

assert not meta2.voided_by or voided_hash not in meta2.voided_by
if tx2.hash != tx.hash and meta2.conflict_with and not meta2.voided_by:
Expand Down
36 changes: 7 additions & 29 deletions hathor/indexes/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from hathor.indexes.nc_creation_index import NCCreationIndex
from hathor.indexes.nc_history_index import NCHistoryIndex
from hathor.indexes.timestamp_index import ScopeType as TimestampScopeType, TimestampIndex
from hathor.indexes.tips_index import ScopeType as TipsScopeType, TipsIndex
from hathor.indexes.tokens_index import TokensIndex
from hathor.indexes.utxo_index import UtxoIndex
from hathor.transaction import BaseTransaction
Expand Down Expand Up @@ -60,9 +59,6 @@ class IndexesManager(ABC):
log = get_logger()

info: InfoIndex
all_tips: TipsIndex
block_tips: TipsIndex
tx_tips: TipsIndex

sorted_all: TimestampIndex
sorted_blocks: TimestampIndex
Expand Down Expand Up @@ -94,9 +90,6 @@ def iter_all_indexes(self) -> Iterator[BaseIndex]:
""" Iterate over all of the indexes abstracted by this manager, hiding their specific implementation details"""
return filter(None, [
self.info,
self.all_tips,
self.block_tips,
self.tx_tips,
self.sorted_all,
self.sorted_blocks,
self.sorted_txs,
Expand Down Expand Up @@ -356,20 +349,12 @@ def add_tx(self, tx: BaseTransaction) -> bool:
"""
self.info.update_timestamps(tx)

# These two calls return False when a transaction changes from
# voided to executed and vice-versa.
r1 = self.all_tips.add_tx(tx)
r2 = self.sorted_all.add_tx(tx)
assert r1 == r2
r1 = self.sorted_all.add_tx(tx)

if tx.is_block:
r3 = self.block_tips.add_tx(tx)
r4 = self.sorted_blocks.add_tx(tx)
assert r3 == r4
r2 = self.sorted_blocks.add_tx(tx)
else:
r3 = self.tx_tips.add_tx(tx)
r4 = self.sorted_txs.add_tx(tx)
assert r3 == r4
r2 = self.sorted_txs.add_tx(tx)

if self.addresses:
self.addresses.add_tx(tx)
Expand All @@ -390,10 +375,10 @@ def add_tx(self, tx: BaseTransaction) -> bool:

# We need to check r1 as well to make sure we don't count twice the transactions/blocks that are
# just changing from voided to executed or vice-versa
if r1 and r3:
if r1 and r2:
self.info.update_counts(tx)

return r3
return r2

def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert: bool = False) -> None:
""" Delete a transaction from the indexes
Expand All @@ -404,9 +389,8 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:

if remove_all:
# We delete from indexes in two cases: (i) mark tx as voided, and (ii) remove tx.
# We only remove tx from all_tips and sorted_all when it is removed from the storage.
# For clarity, when a tx is marked as voided, it is not removed from all_tips and sorted_all.
self.all_tips.del_tx(tx, relax_assert=relax_assert)
# We only remove tx from sorted_all when it is removed from the storage.
# For clarity, when a tx is marked as voided, it is not removed from sorted_all.
self.sorted_all.del_tx(tx)
if self.addresses:
self.addresses.remove_tx(tx)
Expand All @@ -428,10 +412,8 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
self.mempool_tips.update(tx, force_remove=True)

if tx.is_block:
self.block_tips.del_tx(tx, relax_assert=relax_assert)
self.sorted_blocks.del_tx(tx)
else:
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
self.sorted_txs.del_tx(tx)

if self.tokens:
Expand All @@ -440,7 +422,6 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:

class RocksDBIndexesManager(IndexesManager):
def __init__(self, rocksdb_storage: 'RocksDBStorage', *, settings: HathorSettings) -> None:
from hathor.indexes.partial_rocksdb_tips_index import PartialRocksDBTipsIndex
from hathor.indexes.rocksdb_height_index import RocksDBHeightIndex
from hathor.indexes.rocksdb_info_index import RocksDBInfoIndex
from hathor.indexes.rocksdb_timestamp_index import RocksDBTimestampIndex
Expand All @@ -450,9 +431,6 @@ def __init__(self, rocksdb_storage: 'RocksDBStorage', *, settings: HathorSetting

self.info = RocksDBInfoIndex(self._db, settings=settings)
self.height = RocksDBHeightIndex(self._db, settings=settings)
self.all_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.ALL, settings=settings)
self.block_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.BLOCKS, settings=settings)
self.tx_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.TXS, settings=settings)

self.sorted_all = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.ALL, settings=settings)
self.sorted_blocks = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.BLOCKS, settings=settings)
Expand Down
Loading