Skip to content
Closed
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/transaction/resources/create_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def _verify_unsigned_skip_pow(self, tx: Transaction) -> None:
# need to run verify_inputs first to check if all inputs exist
verifiers.tx.verify_inputs(tx, deps, skip_script=True)
verifiers.vertex.verify_parents(tx, deps)
verifiers.tx.verify_sum(tx.get_complete_token_info())
verifiers.tx.verify_sum(deps)


CreateTxResource.openapi = {
Expand Down
13 changes: 10 additions & 3 deletions hathor/transaction/storage/simple_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ class SimpleMemoryStorage:
Instances of this class simply facilitate storing some data in memory, specifically for pre-fetched verification
dependencies.
"""
__slots__ = ('_blocks', '_transactions',)
__slots__ = ('_blocks', '_transactions', '_best_block_tips')

def __init__(self) -> None:
self._blocks: dict[VertexId, BaseTransaction] = {}
self._transactions: dict[VertexId, BaseTransaction] = {}
self._best_block_tips: list[VertexId] = []

@property
def _vertices(self) -> dict[VertexId, BaseTransaction]:
Expand Down Expand Up @@ -101,6 +102,12 @@ def add_vertex(self, vertex: BaseTransaction) -> None:

raise NotImplementedError

def set_best_block_tips_from_storage(self, storage: TransactionStorage) -> None:
"""Get the best block tips from a storage and save them in this instance."""
tips = storage.get_best_block_tips()
self.add_vertices_from_storage(storage, tips)
self._best_block_tips = tips

def get_best_block_tips(self) -> list[VertexId]:
# TODO: Currently unused, will be implemented in a next PR.
raise NotImplementedError
"""Return the best block saved in this instance."""
return self._best_block_tips
9 changes: 7 additions & 2 deletions hathor/transaction/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from hathor.reward_lock import iter_spent_rewards
from hathor.transaction import BaseTransaction, TxInput, TxOutput, TxVersion
from hathor.transaction.base_transaction import TX_HASH_SIZE
from hathor.transaction.exceptions import InvalidToken
from hathor.transaction.exceptions import InexistentInput, InvalidToken
from hathor.transaction.util import VerboseCallback, unpack, unpack_len
from hathor.types import TokenUid, VertexId
from hathor.util import not_none
Expand Down Expand Up @@ -303,7 +303,12 @@ def _get_token_info_from_inputs(self) -> dict[TokenUid, TokenInfo]:

for tx_input in self.inputs:
spent_tx = self.get_spent_tx(tx_input)
spent_output = spent_tx.outputs[tx_input.index]
try:
spent_output = spent_tx.outputs[tx_input.index]
except IndexError:
raise InexistentInput(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you had this exception raised? I mean, this condition would be impossible if the transaction has been validated.

No problem with this change. Just checking if this condition occurred with you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just that before, this method was only called during verification. Now, it's called before verification (when retrieving the verification dependencies). So, if the tx is invalid, it will raise this exception during that retrieval.

f'Output spent by this input does not exist: {tx_input.tx_id.hex()} index {tx_input.index}'
)

token_uid = spent_tx.get_token_uid(spent_output.get_token_index())
(amount, can_mint, can_melt) = token_dict.get(token_uid, default_info)
Expand Down
8 changes: 4 additions & 4 deletions hathor/verification/token_creation_transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from hathor.conf.settings import HathorSettings
from hathor.transaction.exceptions import InvalidToken, TransactionDataError
from hathor.transaction.token_creation_tx import TokenCreationTransaction
from hathor.transaction.transaction import TokenInfo
from hathor.transaction.util import clean_token_string
from hathor.types import TokenUid
from hathor.util import not_none
from hathor.verification.verification_dependencies import TransactionDependencies


class TokenCreationTransactionVerifier:
Expand All @@ -26,7 +26,7 @@ class TokenCreationTransactionVerifier:
def __init__(self, *, settings: HathorSettings) -> None:
self._settings = settings

def verify_minted_tokens(self, tx: TokenCreationTransaction, token_dict: dict[TokenUid, TokenInfo]) -> None:
def verify_minted_tokens(self, tx: TokenCreationTransaction, tx_deps: TransactionDependencies) -> None:
""" Besides all checks made on regular transactions, a few extra ones are made:
- only HTR tokens on the inputs;
- new tokens are actually being minted;
Expand All @@ -35,7 +35,7 @@ def verify_minted_tokens(self, tx: TokenCreationTransaction, token_dict: dict[To
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
"""
# make sure tokens are being minted
token_info = token_dict[tx.hash]
token_info = tx_deps.token_info[not_none(tx.hash)]
if token_info.amount <= 0:
raise InvalidToken('Token creation transaction must mint new tokens')

Expand Down
9 changes: 4 additions & 5 deletions hathor/verification/transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@
TooManySigOps,
WeightError,
)
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.types import VertexId
from hathor.verification.verification_dependencies import TransactionDependencies

cpu = get_cpu_profiler()
Expand Down Expand Up @@ -206,7 +205,7 @@ def verify_output_token_indexes(self, tx: Transaction) -> None:
if output.get_token_index() > len(tx.tokens):
raise InvalidToken('token uid index not available: index {}'.format(output.get_token_index()))

def verify_sum(self, token_dict: dict[TokenUid, TokenInfo]) -> None:
def verify_sum(self, tx_deps: TransactionDependencies) -> None:
"""Verify that the sum of outputs is equal of the sum of inputs, for each token. If sum of inputs
and outputs is not 0, make sure inputs have mint/melt authority.

Expand All @@ -219,7 +218,7 @@ def verify_sum(self, token_dict: dict[TokenUid, TokenInfo]) -> None:
"""
withdraw = 0
deposit = 0
for token_uid, token_info in token_dict.items():
for token_uid, token_info in tx_deps.token_info.items():
if token_uid == self._settings.HATHOR_TOKEN_UID:
continue

Expand All @@ -241,7 +240,7 @@ def verify_sum(self, token_dict: dict[TokenUid, TokenInfo]) -> None:

# check whether the deposit/withdraw amount is correct
htr_expected_amount = withdraw - deposit
htr_info = token_dict[self._settings.HATHOR_TOKEN_UID]
htr_info = tx_deps.token_info[self._settings.HATHOR_TOKEN_UID]
if htr_info.amount != htr_expected_amount:
raise InputOutputMismatch('HTR balance is different than expected. (amount={}, expected={})'.format(
htr_info.amount,
Expand Down
12 changes: 10 additions & 2 deletions hathor/verification/verification_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
from hathor.feature_activation.model.feature_description import FeatureInfo
from hathor.transaction import Block
from hathor.transaction.storage.simple_memory_storage import SimpleMemoryStorage
from hathor.transaction.transaction import Transaction
from hathor.transaction.transaction import TokenInfo, Transaction
from hathor.types import TokenUid


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -72,17 +73,24 @@ def create(cls, block: Block, feature_service: FeatureService) -> Self:
)


@dataclass(frozen=True, slots=True)
class TransactionDependencies(VertexDependencies):
"""A dataclass of dependencies necessary for transaction verification."""
token_info: dict[TokenUid, TokenInfo]

@classmethod
def create(cls, tx: Transaction) -> Self:
"""Create a transaction dependencies instance."""
assert tx.storage is not None
token_info = tx.get_complete_token_info()
simple_storage = SimpleMemoryStorage()
spent_txs = [tx_input.tx_id for tx_input in tx.inputs]
deps = tx.parents + spent_txs

simple_storage.add_vertices_from_storage(tx.storage, deps)
simple_storage.set_best_block_tips_from_storage(tx.storage)

return cls(storage=simple_storage)
return cls(
storage=simple_storage,
token_info=token_info
)
12 changes: 4 additions & 8 deletions hathor/verification/verification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
from hathor.profiler import get_cpu_profiler
from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction, TxVersion
from hathor.transaction.token_creation_tx import TokenCreationTransaction
from hathor.transaction.transaction import TokenInfo
from hathor.transaction.validation_state import ValidationState
from hathor.types import TokenUid
from hathor.verification.verification_dependencies import (
BasicBlockDependencies,
BlockDependencies,
Expand Down Expand Up @@ -219,7 +217,6 @@ def _verify_tx(
tx_deps: TransactionDependencies,
*,
reject_locked_reward: bool,
token_dict: dict[TokenUid, TokenInfo] | None = None
) -> None:
""" Common verification for all transactions:
(i) number of inputs is at most 256
Expand All @@ -236,9 +233,9 @@ def _verify_tx(
self.verifiers.tx.verify_sigops_input(tx, tx_deps)
self.verifiers.tx.verify_inputs(tx, tx_deps) # need to run verify_inputs first to check if all inputs exist
self.verifiers.vertex.verify_parents(tx, tx_deps)
self.verifiers.tx.verify_sum(token_dict or tx.get_complete_token_info())
self.verifiers.tx.verify_sum(tx_deps)
if reject_locked_reward:
self.verifiers.tx.verify_reward_locked(tx)
self.verifiers.tx.verify_reward_locked(tx, tx_deps)

def _verify_token_creation_tx(
self,
Expand All @@ -251,9 +248,8 @@ def _verify_token_creation_tx(

We also overload verify_sum to make some different checks
"""
token_dict = tx.get_complete_token_info()
self._verify_tx(tx, tx_deps, reject_locked_reward=reject_locked_reward, token_dict=token_dict)
self.verifiers.token_creation_tx.verify_minted_tokens(tx, token_dict)
self._verify_tx(tx, tx_deps, reject_locked_reward=reject_locked_reward)
self.verifiers.token_creation_tx.verify_minted_tokens(tx, tx_deps)
self.verifiers.token_creation_tx.verify_token_info(tx)

def verify_without_storage(self, vertex: BaseTransaction) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/tx/test_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_tx_token_outputs(self):
tx.inputs[0].data = P2PKH.create_input_data(public_bytes, signature)
self.manager.cpu_mining_service.resolve(tx)
with self.assertRaises(InvalidToken):
self.manager.verification_service.verify(tx)
self.manager.verification_service.verifiers.tx.verify_output_token_indexes(tx)

# with 1 token uid in list
tx.tokens = [bytes.fromhex('0023be91834c973d6a6ddd1a0ae411807b7c8ef2a015afb5177ee64b666ce602')]
Expand All @@ -82,7 +82,7 @@ def test_tx_token_outputs(self):
tx.inputs[0].data = P2PKH.create_input_data(public_bytes, signature)
self.manager.cpu_mining_service.resolve(tx)
with self.assertRaises(InvalidToken):
self.manager.verification_service.verify(tx)
self.manager.verification_service.verifiers.tx.verify_output_token_indexes(tx)

# try hathor authority UTXO
output = TxOutput(value, script, 0b10000000)
Expand Down
5 changes: 3 additions & 2 deletions tests/tx/test_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ def test_input_output_match(self):
public_bytes, signature = self.wallet.get_input_aux_data(data_to_sign, self.genesis_private_key)
_input.data = P2PKH.create_input_data(public_bytes, signature)

deps = TransactionDependencies.create(tx)
with self.assertRaises(InputOutputMismatch):
self._verifiers.tx.verify_sum(tx.get_complete_token_info())
self._verifiers.tx.verify_sum(deps)

def test_validation(self):
# add 100 blocks and check that walking through get_next_block_best_chain yields the same blocks
Expand Down Expand Up @@ -540,7 +541,7 @@ def test_tx_inputs_out_of_range(self):
self.manager.verification_service.verify(tx)

with self.assertRaises(InexistentInput):
self._verifiers.tx.verify_inputs(tx, TransactionDependencies(SimpleMemoryStorage()))
self._verifiers.tx.verify_inputs(tx, TransactionDependencies(SimpleMemoryStorage(), Mock()))

def test_tx_inputs_conflict(self):
# the new tx inputs will try to spend the same output
Expand Down