diff --git a/hathor/transaction/resources/create_tx.py b/hathor/transaction/resources/create_tx.py index 9db505391..521b675ec 100644 --- a/hathor/transaction/resources/create_tx.py +++ b/hathor/transaction/resources/create_tx.py @@ -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 = { diff --git a/hathor/transaction/storage/simple_memory_storage.py b/hathor/transaction/storage/simple_memory_storage.py index 3baa6deef..a86969b67 100644 --- a/hathor/transaction/storage/simple_memory_storage.py +++ b/hathor/transaction/storage/simple_memory_storage.py @@ -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]: @@ -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 diff --git a/hathor/transaction/transaction.py b/hathor/transaction/transaction.py index a9d9fec5a..cd5c31f66 100644 --- a/hathor/transaction/transaction.py +++ b/hathor/transaction/transaction.py @@ -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 @@ -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( + 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) diff --git a/hathor/verification/token_creation_transaction_verifier.py b/hathor/verification/token_creation_transaction_verifier.py index 4d0ac543c..8309f6416 100644 --- a/hathor/verification/token_creation_transaction_verifier.py +++ b/hathor/verification/token_creation_transaction_verifier.py @@ -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: @@ -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; @@ -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') diff --git a/hathor/verification/transaction_verifier.py b/hathor/verification/transaction_verifier.py index 73cc05d62..b6ba43dc8 100644 --- a/hathor/verification/transaction_verifier.py +++ b/hathor/verification/transaction_verifier.py @@ -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() @@ -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. @@ -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 @@ -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, diff --git a/hathor/verification/verification_dependencies.py b/hathor/verification/verification_dependencies.py index aa4dece05..2255a9a67 100644 --- a/hathor/verification/verification_dependencies.py +++ b/hathor/verification/verification_dependencies.py @@ -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) @@ -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 + ) diff --git a/hathor/verification/verification_service.py b/hathor/verification/verification_service.py index 9898e4280..6caaf0f71 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -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, @@ -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 @@ -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, @@ -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: diff --git a/tests/tx/test_tokens.py b/tests/tx/test_tokens.py index 0906477e1..b94eb4736 100644 --- a/tests/tx/test_tokens.py +++ b/tests/tx/test_tokens.py @@ -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')] @@ -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) diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index 40b273ca8..6dc5e111a 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -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 @@ -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