Skip to content

refactor(verification): reorganize min_height verification #1008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 30, 2024
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/consensus/transaction_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def check_conflicts(self, tx: Transaction) -> None:

# If we got here, either it was a tie or we won.
# So, let's void the conflict txs.
for conflict_tx in conflict_list:
for conflict_tx in sorted(conflict_list, key=lambda x: x.timestamp, reverse=True):
self.mark_as_voided(conflict_tx)

if not tie_list:
Expand Down
6 changes: 0 additions & 6 deletions hathor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,12 +914,6 @@ def push_tx(self, tx: Transaction, allow_non_standard_script: bool = False,
if not tx_from_lib.is_standard(max_output_script_size, not allow_non_standard_script):
raise NonStandardTxError('Transaction is non standard.')

# Validate tx.
try:
self.verification_service.verify(tx)
except TxValidationError as e:
raise InvalidNewTransaction(str(e))

self.propagate_tx(tx, fails_silently=False)

def propagate_tx(self, tx: BaseTransaction, fails_silently: bool = True) -> bool:
Expand Down
12 changes: 9 additions & 3 deletions hathor/reward_lock/reward_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,16 @@ def get_spent_reward_locked_info(tx: 'Transaction', storage: 'VertexStorageProto
"""Check if any input block reward is locked, returning the locked information if any, or None if they are all
unlocked."""
from hathor.transaction.transaction import RewardLockedInfo
best_height = get_minimum_best_height(storage)
for blk in iter_spent_rewards(tx, storage):
needed_height = _spent_reward_needed_height(blk, storage)
needed_height = _spent_reward_needed_height(blk, best_height)
if needed_height > 0:
return RewardLockedInfo(blk.hash, needed_height)
return None


def _spent_reward_needed_height(block: Block, storage: 'VertexStorageProtocol') -> int:
""" Returns height still needed to unlock this `block` reward: 0 means it's unlocked."""
def get_minimum_best_height(storage: 'VertexStorageProtocol') -> int:
"""Return the height of the current best block that shall be used for `min_height` verification."""
import math

# omitting timestamp to get the current best block, this will usually hit the cache instead of being slow
Expand All @@ -61,6 +62,11 @@ def _spent_reward_needed_height(block: Block, storage: 'VertexStorageProtocol')
blk = storage.get_block(tip)
best_height = min(best_height, blk.get_height())
assert isinstance(best_height, int)
return best_height


def _spent_reward_needed_height(block: Block, best_height: int) -> int:
""" Returns height still needed to unlock this `block` reward: 0 means it's unlocked."""
spent_height = block.get_height()
spend_blocks = best_height - spent_height
settings = get_global_settings()
Expand Down
6 changes: 1 addition & 5 deletions hathor/simulator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@
from hathor.types import Address, VertexId


def gen_new_tx(manager: HathorManager, address: str, value: int, verify: bool = True) -> Transaction:
def gen_new_tx(manager: HathorManager, address: str, value: int) -> Transaction:
"""
Generate and return a new transaction.

Args:
manager: the HathorManager to generate the transaction for
address: an address for the transaction's output
value: a value for the transaction's output
verify: whether to verify the generated transaction

Returns: the generated transaction.
"""
Expand All @@ -48,8 +47,6 @@ def gen_new_tx(manager: HathorManager, address: str, value: int, verify: bool =
tx.weight = 1
tx.parents = manager.get_new_tx_parents(tx.timestamp)
manager.cpu_mining_service.resolve(tx)
if verify:
manager.verification_service.verify(tx)
return tx


Expand Down Expand Up @@ -111,7 +108,6 @@ def add_new_block(
if signal_bits is not None:
block.signal_bits = signal_bits
manager.cpu_mining_service.resolve(block)
manager.verification_service.validate_full(block)
if propagate:
manager.propagate_tx(block, fails_silently=False)
if advance_clock:
Expand Down
18 changes: 14 additions & 4 deletions hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,13 +624,14 @@ def get_metadata(self, *, force_reload: bool = False, use_storage: bool = True)
# happens include generating new mining blocks and some tests
height = self.calculate_height() if self.storage else None
score = self.weight if self.is_genesis else 0
min_height = 0 if self.is_genesis else None

metadata = TransactionMetadata(
hash=self._hash,
accumulated_weight=self.weight,
height=height,
score=score,
min_height=0,
min_height=min_height
)
self._metadata = metadata
if not metadata.hash:
Expand Down Expand Up @@ -713,8 +714,9 @@ def update_initial_metadata(self, *, save: bool = True) -> None:
"""
self._update_height_metadata()
self._update_parents_children_metadata()
self._update_reward_lock_metadata()
self.update_reward_lock_metadata()
self._update_feature_activation_bit_counts()
self._update_initial_accumulated_weight()
if save:
assert self.storage is not None
self.storage.save_transaction(self, only_metadata=True)
Expand All @@ -724,10 +726,13 @@ def _update_height_metadata(self) -> None:
meta = self.get_metadata()
meta.height = self.calculate_height()

def _update_reward_lock_metadata(self) -> None:
def update_reward_lock_metadata(self) -> None:
"""Update the txs/block min_height metadata."""
metadata = self.get_metadata()
metadata.min_height = self.calculate_min_height()
min_height = self.calculate_min_height()
if metadata.min_height is not None:
assert metadata.min_height == min_height
metadata.min_height = min_height

def _update_parents_children_metadata(self) -> None:
"""Update the txs/block parent's children metadata."""
Expand All @@ -749,6 +754,11 @@ def _update_feature_activation_bit_counts(self) -> None:
# This method lazily calculates and stores the value in metadata
self.get_feature_activation_bit_counts()

def _update_initial_accumulated_weight(self) -> None:
"""Update the vertex initial accumulated_weight."""
metadata = self.get_metadata()
metadata.accumulated_weight = self.weight

def update_timestamp(self, now: int) -> None:
"""Update this tx's timestamp

Expand Down
11 changes: 7 additions & 4 deletions hathor/transaction/storage/transaction_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from hathor.pubsub import PubSubManager
from hathor.transaction.base_transaction import BaseTransaction, TxOutput
from hathor.transaction.block import Block
from hathor.transaction.exceptions import RewardLocked
from hathor.transaction.storage.exceptions import (
TransactionDoesNotExist,
TransactionIsNotABlock,
Expand All @@ -49,6 +50,7 @@
from hathor.transaction.transaction import Transaction
from hathor.transaction.transaction_metadata import TransactionMetadata
from hathor.types import VertexId
from hathor.verification.transaction_verifier import TransactionVerifier

cpu = get_cpu_profiler()

Expand Down Expand Up @@ -1097,10 +1099,11 @@ def compute_transactions_that_became_invalid(self, new_best_height: int) -> list
from hathor.transaction.validation_state import ValidationState
to_remove: list[BaseTransaction] = []
for tx in self.iter_mempool_from_best_index():
tx_min_height = tx.get_metadata().min_height
assert tx_min_height is not None
# We use +1 here because a tx is valid if it can be confirmed by the next block
if new_best_height + 1 < tx_min_height:
try:
TransactionVerifier.verify_reward_locked_for_height(
tx, new_best_height, assert_min_height_verification=False
)
except RewardLocked:
tx.set_validation(ValidationState.INVALID)
to_remove.append(tx)
return to_remove
Expand Down
47 changes: 43 additions & 4 deletions hathor/verification/transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from hathor.daa import DifficultyAdjustmentAlgorithm
from hathor.profiler import get_cpu_profiler
from hathor.reward_lock import get_spent_reward_locked_info
from hathor.reward_lock.reward_lock import get_minimum_best_height
from hathor.transaction import BaseTransaction, Transaction, TxInput
from hathor.transaction.exceptions import (
ConflictingInputs,
Expand All @@ -37,7 +38,6 @@
from hathor.transaction.transaction import TokenInfo
from hathor.transaction.util import get_deposit_amount, get_withdraw_amount
from hathor.types import TokenUid, VertexId
from hathor.util import not_none

cpu = get_cpu_profiler()

Expand Down Expand Up @@ -142,12 +142,51 @@ def verify_script(self, *, tx: Transaction, input_tx: TxInput, spent_tx: BaseTra
raise InvalidInputData(e) from e

def verify_reward_locked(self, tx: Transaction) -> None:
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only
the block rewards spent by this tx itself, and not the inherited `min_height`."""
info = get_spent_reward_locked_info(tx, not_none(tx.storage))
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering both
the block rewards spent by this tx itself, and the inherited `min_height`."""
assert tx.storage is not None
best_height = get_minimum_best_height(tx.storage)
self.verify_reward_locked_for_height(tx, best_height)

@staticmethod
def verify_reward_locked_for_height(
tx: Transaction,
best_height: int,
*,
assert_min_height_verification: bool = True
) -> None:
"""
Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering both
the block rewards spent by this tx itself, and the inherited `min_height`.

Args:
tx: the transaction to be verified.
best_height: the height of the best chain to be used for verification.
assert_min_height_verification: whether the inherited `min_height` verification must pass.

Note: for verification of new transactions, `assert_min_height_verification` must be `True`. This
verification is always expected to pass for new txs, as a failure would mean one of its dependencies would
have failed too. So an `AssertionError` is raised if it fails.

However, when txs are being re-verified for Reward Lock during a reorg, it's possible that txs may fail
their inherited `min_height` verification. So in that case `assert_min_height_verification` is `False`,
and a normal `RewardLocked` exception is raised instead.
"""
assert tx.storage is not None
info = get_spent_reward_locked_info(tx, tx.storage)
if info is not None:
raise RewardLocked(f'Reward {info.block_hash.hex()} still needs {info.blocks_needed} to be unlocked.')

meta = tx.get_metadata()
assert meta.min_height is not None
# We use +1 here because a tx is valid if it can be confirmed by the next block
if best_height + 1 < meta.min_height:
if assert_min_height_verification:
raise AssertionError('a new tx should never be invalid by its inherited min_height.')
raise RewardLocked(
f'Tx {tx.hash_hex} has min_height={meta.min_height}, but the best_height={best_height}.'
)

def verify_number_of_inputs(self, tx: Transaction) -> None:
"""Verify number of inputs is in a valid range"""
if len(tx.inputs) > self._settings.MAX_NUM_INPUTS:
Expand Down
4 changes: 3 additions & 1 deletion hathor/vertex_handler/vertex_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,13 @@ def _validate_vertex(
return False

if not metadata.validation.is_fully_connected():
# TODO: Remove this from here after a refactor in metadata initialization
vertex.update_reward_lock_metadata()
try:
self._verification_service.validate_full(vertex, reject_locked_reward=reject_locked_reward)
except HathorError as e:
if not fails_silently:
raise InvalidNewTransaction('full validation failed') from e
raise InvalidNewTransaction(f'full validation failed: {str(e)}') from e
self._log.warn('on_new_tx(): full validation failed', tx=vertex.hash_hex, exc_info=True)
return False

Expand Down
1 change: 1 addition & 0 deletions hathor/wallet/resources/send_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def _render_POST_thread(self, values: dict[str, Any], request: Request) -> Union
weight = self.manager.daa.minimum_tx_weight(tx)
tx.weight = weight
self.manager.cpu_mining_service.resolve(tx)
tx.update_reward_lock_metadata()
self.manager.verification_service.verify(tx)
return tx

Expand Down
1 change: 1 addition & 0 deletions hathor/wallet/resources/thin_wallet/send_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def _should_stop():
if context.should_stop_mining_thread:
raise CancelledError()
context.tx.update_hash()
context.tx.update_reward_lock_metadata()
self.manager.verification_service.verify(context.tx)
return context

Expand Down
4 changes: 0 additions & 4 deletions tests/consensus/test_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ def test_revert_block_high_weight(self) -> None:
b0 = tb0.generate_mining_block(manager.rng, storage=manager.tx_storage)
b0.weight = 10
manager.cpu_mining_service.resolve(b0)
manager.verification_service.verify(b0)
manager.propagate_tx(b0, fails_silently=False)

b1 = add_new_block(manager, advance_clock=15)
Expand Down Expand Up @@ -143,7 +142,6 @@ def test_dont_revert_block_low_weight(self) -> None:
b0 = manager.generate_mining_block()
b0.parents = [blocks[-1].hash, conflicting_tx.hash, conflicting_tx.parents[0]]
manager.cpu_mining_service.resolve(b0)
manager.verification_service.verify(b0)
manager.propagate_tx(b0, fails_silently=False)

b1 = add_new_block(manager, advance_clock=15)
Expand Down Expand Up @@ -199,7 +197,6 @@ def test_dont_revert_block_high_weight_transaction_verify_other(self) -> None:
b0 = tb0.generate_mining_block(manager.rng, storage=manager.tx_storage)
b0.weight = 10
manager.cpu_mining_service.resolve(b0)
manager.verification_service.verify(b0)
manager.propagate_tx(b0, fails_silently=False)

b1 = add_new_block(manager, advance_clock=15)
Expand Down Expand Up @@ -253,7 +250,6 @@ def test_dont_revert_block_high_weight_verify_both(self) -> None:
b0.parents = [b0.parents[0], conflicting_tx.hash, conflicting_tx.parents[0]]
b0.weight = 10
manager.cpu_mining_service.resolve(b0)
manager.verification_service.verify(b0)
manager.propagate_tx(b0, fails_silently=False)

b1 = add_new_block(manager, advance_clock=15)
Expand Down
1 change: 0 additions & 1 deletion tests/event/test_event_reorg.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ def test_reorg_events(self) -> None:
b0 = tb0.generate_mining_block(self.manager.rng, storage=self.manager.tx_storage, address=BURN_ADDRESS)
b0.weight = 10
self.manager.cpu_mining_service.resolve(b0)
self.manager.verification_service.verify(b0)
self.manager.propagate_tx(b0, fails_silently=False)
self.log.debug('reorg block propagated')
self.run_to_completion()
Expand Down
1 change: 1 addition & 0 deletions tests/feature_activation/test_feature_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ def test_feature(self) -> None:
non_signaling_block = manager.generate_mining_block()
manager.cpu_mining_service.resolve(non_signaling_block)
non_signaling_block.signal_bits = 0b10
non_signaling_block.update_reward_lock_metadata()

with pytest.raises(BlockMustSignalError):
manager.verification_service.verify(non_signaling_block)
Expand Down
2 changes: 0 additions & 2 deletions tests/p2p/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def _add_new_tx(self, address: str, value: int) -> Transaction:
tx.weight = 10
tx.parents = self.manager1.get_new_tx_parents()
self.manager1.cpu_mining_service.resolve(tx)
self.manager1.verification_service.verify(tx)
self.manager1.propagate_tx(tx)
self.clock.advance(10)
return tx
Expand All @@ -61,7 +60,6 @@ def _add_new_transactions(self, num_txs: int) -> list[Transaction]:
def _add_new_block(self, propagate: bool = True) -> Block:
block: Block = self.manager1.generate_mining_block()
self.assertTrue(self.manager1.cpu_mining_service.resolve(block))
self.manager1.verification_service.verify(block)
self.manager1.on_new_tx(block, propagate_to_peers=propagate)
self.clock.advance(10)
return block
Expand Down
2 changes: 0 additions & 2 deletions tests/p2p/test_sync_mempool.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ def _add_new_tx(self, address: str, value: int) -> Transaction:
tx.weight = 10
tx.parents = self.manager1.get_new_tx_parents()
self.manager1.cpu_mining_service.resolve(tx)
self.manager1.verification_service.verify(tx)
self.manager1.propagate_tx(tx)
self.clock.advance(10)
return tx
Expand All @@ -53,7 +52,6 @@ def _add_new_transactions(self, num_txs: int) -> list[Transaction]:
def _add_new_block(self, propagate: bool = True) -> Block:
block: Block = self.manager1.generate_mining_block()
self.assertTrue(self.manager1.cpu_mining_service.resolve(block))
self.manager1.verification_service.verify(block)
self.manager1.on_new_tx(block, propagate_to_peers=propagate)
self.clock.advance(10)
return block
Expand Down
4 changes: 1 addition & 3 deletions tests/p2p/test_sync_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def test_receiving_tips_limit(self) -> None:
# Custom tx generator that generates tips
parents = manager1.get_new_tx_parents(manager1.tx_storage.latest_timestamp)

def custom_gen_new_tx(manager: HathorManager, _address: str, value: int, verify: bool = True) -> Transaction:
def custom_gen_new_tx(manager: HathorManager, _address: str, value: int) -> Transaction:
outputs = []
# XXX: burn address guarantees that this output will not be used as input for any following transactions
# XXX: reduce value to make sure we can generate more transactions, otherwise it will spend a linear random
Expand All @@ -294,8 +294,6 @@ def custom_gen_new_tx(manager: HathorManager, _address: str, value: int, verify:
# XXX: fixed parents is the final requirement to make all the generated new tips
tx.parents = parents
manager.cpu_mining_service.resolve(tx)
if verify:
manager.verification_service.verify(tx)
return tx

# Generate 100 tx-tips in mempool.
Expand Down
4 changes: 2 additions & 2 deletions tests/resources/transaction/test_mining.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def test_get_block_template_with_address(self):
'accumulated_weight': 1.0,
'score': 0,
'height': 1,
'min_height': 0,
'min_height': None,
'first_block': None,
'feature_activation_bit_counts': None
},
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_get_block_template_without_address(self):
'accumulated_weight': 1.0,
'score': 0,
'height': 1,
'min_height': 0,
'min_height': None,
'first_block': None,
'feature_activation_bit_counts': None
},
Expand Down
Loading
Loading