Skip to content

Commit 2bc5ebd

Browse files
committed
refactor(verification): move transaction-only verification methods
1 parent 727b499 commit 2bc5ebd

File tree

4 files changed

+227
-289
lines changed

4 files changed

+227
-289
lines changed

hathor/transaction/token_creation_tx.py

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
from typing import Any, Optional
1717

1818
from hathor.transaction.base_transaction import TxInput, TxOutput, TxVersion
19-
from hathor.transaction.exceptions import InvalidToken, TransactionDataError
2019
from hathor.transaction.storage import TransactionStorage # noqa: F401
21-
from hathor.transaction.transaction import TokenInfo, Transaction
22-
from hathor.transaction.util import VerboseCallback, clean_token_string, int_to_bytes, unpack, unpack_len
20+
from hathor.transaction.transaction import Transaction
21+
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
2322

2423
# Signal bits (B), version (B), inputs len (B), outputs len (B)
2524
_FUNDS_FORMAT_STRING = '!BBBB'
@@ -220,45 +219,6 @@ def to_json_extended(self) -> dict[str, Any]:
220219
json['tokens'] = []
221220
return json
222221

223-
def verify_sum(self) -> None:
224-
""" Besides all checks made on regular transactions, a few extra ones are made:
225-
- only HTR tokens on the inputs;
226-
- new tokens are actually being minted;
227-
228-
:raises InvalidToken: when there's an error in token operations
229-
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
230-
"""
231-
token_dict = self.get_token_info_from_inputs()
232-
233-
# we add the created token's info to token_dict, as the creation tx allows for mint/melt
234-
assert self.hash is not None
235-
token_dict[self.hash] = TokenInfo(0, True, True)
236-
237-
self.update_token_info_from_outputs(token_dict)
238-
239-
# make sure tokens are being minted
240-
token_info = token_dict[self.hash]
241-
if token_info.amount <= 0:
242-
raise InvalidToken('Token creation transaction must mint new tokens')
243-
244-
self.check_authorities_and_deposit(token_dict)
245-
246-
def verify_token_info(self) -> None:
247-
""" Validates token info
248-
"""
249-
name_len = len(self.token_name)
250-
symbol_len = len(self.token_symbol)
251-
if name_len == 0 or name_len > self._settings.MAX_LENGTH_TOKEN_NAME:
252-
raise TransactionDataError('Invalid token name length ({})'.format(name_len))
253-
if symbol_len == 0 or symbol_len > self._settings.MAX_LENGTH_TOKEN_SYMBOL:
254-
raise TransactionDataError('Invalid token symbol length ({})'.format(symbol_len))
255-
256-
# Can't create token with hathor name or symbol
257-
if clean_token_string(self.token_name) == clean_token_string(self._settings.HATHOR_TOKEN_NAME):
258-
raise TransactionDataError('Invalid token name ({})'.format(self.token_name))
259-
if clean_token_string(self.token_symbol) == clean_token_string(self._settings.HATHOR_TOKEN_SYMBOL):
260-
raise TransactionDataError('Invalid token symbol ({})'.format(self.token_symbol))
261-
262222

263223
def decode_string_utf8(encoded: bytes, key: str) -> str:
264224
""" Raises StructError in case it's not a valid utf-8 string

hathor/transaction/transaction.py

Lines changed: 2 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,13 @@
1717
from struct import pack
1818
from typing import TYPE_CHECKING, Any, Iterator, NamedTuple, Optional
1919

20-
from hathor import daa
2120
from hathor.checkpoint import Checkpoint
2221
from hathor.exception import InvalidNewTransaction
2322
from hathor.profiler import get_cpu_profiler
2423
from hathor.transaction import BaseTransaction, Block, TxInput, TxOutput, TxVersion
2524
from hathor.transaction.base_transaction import TX_HASH_SIZE
26-
from hathor.transaction.exceptions import (
27-
ConflictingInputs,
28-
DuplicatedParents,
29-
IncorrectParents,
30-
InexistentInput,
31-
InputOutputMismatch,
32-
InvalidInputData,
33-
InvalidInputDataSize,
34-
InvalidToken,
35-
NoInputError,
36-
RewardLocked,
37-
ScriptError,
38-
TimestampError,
39-
TooManyInputs,
40-
TooManySigOps,
41-
WeightError,
42-
)
43-
from hathor.transaction.util import VerboseCallback, get_deposit_amount, get_withdraw_amount, unpack, unpack_len
25+
from hathor.transaction.exceptions import InvalidToken
26+
from hathor.transaction.util import VerboseCallback, unpack, unpack_len
4427
from hathor.types import TokenUid, VertexId
4528
from hathor.util import not_none
4629

@@ -296,78 +279,6 @@ def verify_checkpoint(self, checkpoints: list[Checkpoint]) -> None:
296279
raise InvalidNewTransaction(f'Invalid new transaction {self.hash_hex}: expected to reach a checkpoint but '
297280
'none of its children is checkpoint-valid')
298281

299-
def verify_parents_basic(self) -> None:
300-
"""Verify number and non-duplicity of parents."""
301-
assert self.storage is not None
302-
303-
# check if parents are duplicated
304-
parents_set = set(self.parents)
305-
if len(self.parents) > len(parents_set):
306-
raise DuplicatedParents('Tx has duplicated parents: {}', [tx_hash.hex() for tx_hash in self.parents])
307-
308-
if len(self.parents) != 2:
309-
raise IncorrectParents(f'wrong number of parents (tx type): {len(self.parents)}, expecting 2')
310-
311-
def verify_weight(self) -> None:
312-
"""Validate minimum tx difficulty."""
313-
min_tx_weight = daa.minimum_tx_weight(self)
314-
max_tx_weight = min_tx_weight + self._settings.MAX_TX_WEIGHT_DIFF
315-
if self.weight < min_tx_weight - self._settings.WEIGHT_TOL:
316-
raise WeightError(f'Invalid new tx {self.hash_hex}: weight ({self.weight}) is '
317-
f'smaller than the minimum weight ({min_tx_weight})')
318-
elif min_tx_weight > self._settings.MAX_TX_WEIGHT_DIFF_ACTIVATION and self.weight > max_tx_weight:
319-
raise WeightError(f'Invalid new tx {self.hash_hex}: weight ({self.weight}) is '
320-
f'greater than the maximum allowed ({max_tx_weight})')
321-
322-
def verify_unsigned_skip_pow(self) -> None:
323-
""" Same as .verify but skipping pow and signature verification."""
324-
self.verify_number_of_inputs()
325-
self.verify_number_of_outputs()
326-
self.verify_outputs()
327-
self.verify_sigops_output()
328-
self.verify_sigops_input()
329-
self.verify_inputs(skip_script=True) # need to run verify_inputs first to check if all inputs exist
330-
self.verify_parents()
331-
self.verify_sum()
332-
333-
def verify_without_storage(self) -> None:
334-
""" Run all verifications that do not need a storage.
335-
"""
336-
self.verify_pow()
337-
self.verify_number_of_inputs()
338-
self.verify_outputs()
339-
self.verify_sigops_output()
340-
341-
def verify_number_of_inputs(self) -> None:
342-
"""Verify number of inputs is in a valid range"""
343-
if len(self.inputs) > self._settings.MAX_NUM_INPUTS:
344-
raise TooManyInputs('Maximum number of inputs exceeded')
345-
346-
if len(self.inputs) == 0:
347-
if not self.is_genesis:
348-
raise NoInputError('Transaction must have at least one input')
349-
350-
def verify_sigops_input(self) -> None:
351-
""" Count sig operations on all inputs and verify that the total sum is below the limit
352-
"""
353-
from hathor.transaction.scripts import get_sigops_count
354-
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
355-
n_txops = 0
356-
for tx_input in self.inputs:
357-
try:
358-
spent_tx = self.get_spent_tx(tx_input)
359-
except TransactionDoesNotExist:
360-
raise InexistentInput('Input tx does not exist: {}'.format(tx_input.tx_id.hex()))
361-
assert spent_tx.hash is not None
362-
if tx_input.index >= len(spent_tx.outputs):
363-
raise InexistentInput('Output spent by this input does not exist: {} index {}'.format(
364-
tx_input.tx_id.hex(), tx_input.index))
365-
n_txops += get_sigops_count(tx_input.data, spent_tx.outputs[tx_input.index].script)
366-
367-
if n_txops > self._settings.MAX_TX_SIGOPS_INPUT:
368-
raise TooManySigOps(
369-
'TX[{}]: Max number of sigops for inputs exceeded ({})'.format(self.hash_hex, n_txops))
370-
371282
def verify_outputs(self) -> None:
372283
"""Verify outputs reference an existing token uid in the tokens list
373284
@@ -406,92 +317,6 @@ def get_token_info_from_inputs(self) -> dict[TokenUid, TokenInfo]:
406317

407318
return token_dict
408319

409-
def update_token_info_from_outputs(self, token_dict: dict[TokenUid, TokenInfo]) -> None:
410-
"""Iterate over the outputs and add values to token info dict. Updates the dict in-place.
411-
412-
Also, checks if no token has authorities on the outputs not present on the inputs
413-
414-
:raises InvalidToken: when there's an error in token operations
415-
"""
416-
# iterate over outputs and add values to token_dict
417-
for index, tx_output in enumerate(self.outputs):
418-
token_uid = self.get_token_uid(tx_output.get_token_index())
419-
token_info = token_dict.get(token_uid)
420-
if token_info is None:
421-
raise InvalidToken('no inputs for token {}'.format(token_uid.hex()))
422-
else:
423-
# for authority outputs, make sure the same capability (mint/melt) was present in the inputs
424-
if tx_output.can_mint_token() and not token_info.can_mint:
425-
raise InvalidToken('output has mint authority, but no input has it: {}'.format(
426-
tx_output.to_human_readable()))
427-
if tx_output.can_melt_token() and not token_info.can_melt:
428-
raise InvalidToken('output has melt authority, but no input has it: {}'.format(
429-
tx_output.to_human_readable()))
430-
431-
if tx_output.is_token_authority():
432-
# make sure we only have authorities that we know of
433-
if tx_output.value > TxOutput.ALL_AUTHORITIES:
434-
raise InvalidToken('Invalid authorities in output (0b{0:b})'.format(tx_output.value))
435-
else:
436-
# for regular outputs, just subtract from the total amount
437-
sum_tokens = token_info.amount + tx_output.value
438-
token_dict[token_uid] = TokenInfo(sum_tokens, token_info.can_mint, token_info.can_melt)
439-
440-
def check_authorities_and_deposit(self, token_dict: dict[TokenUid, TokenInfo]) -> None:
441-
"""Verify that the sum of outputs is equal of the sum of inputs, for each token. If sum of inputs
442-
and outputs is not 0, make sure inputs have mint/melt authority.
443-
444-
token_dict sums up all tokens present in the tx and their properties (amount, can_mint, can_melt)
445-
amount = outputs - inputs, thus:
446-
- amount < 0 when melting
447-
- amount > 0 when minting
448-
449-
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
450-
"""
451-
withdraw = 0
452-
deposit = 0
453-
for token_uid, token_info in token_dict.items():
454-
if token_uid == self._settings.HATHOR_TOKEN_UID:
455-
continue
456-
457-
if token_info.amount == 0:
458-
# that's the usual behavior, nothing to do
459-
pass
460-
elif token_info.amount < 0:
461-
# tokens have been melted
462-
if not token_info.can_melt:
463-
raise InputOutputMismatch('{} {} tokens melted, but there is no melt authority input'.format(
464-
token_info.amount, token_uid.hex()))
465-
withdraw += get_withdraw_amount(token_info.amount)
466-
else:
467-
# tokens have been minted
468-
if not token_info.can_mint:
469-
raise InputOutputMismatch('{} {} tokens minted, but there is no mint authority input'.format(
470-
(-1) * token_info.amount, token_uid.hex()))
471-
deposit += get_deposit_amount(token_info.amount)
472-
473-
# check whether the deposit/withdraw amount is correct
474-
htr_expected_amount = withdraw - deposit
475-
htr_info = token_dict[self._settings.HATHOR_TOKEN_UID]
476-
if htr_info.amount != htr_expected_amount:
477-
raise InputOutputMismatch('HTR balance is different than expected. (amount={}, expected={})'.format(
478-
htr_info.amount,
479-
htr_expected_amount,
480-
))
481-
482-
def verify_sum(self) -> None:
483-
"""Verify that the sum of outputs is equal of the sum of inputs, for each token.
484-
485-
If there are authority UTXOs involved, tokens can be minted or melted, so the above rule may
486-
not be respected.
487-
488-
:raises InvalidToken: when there's an error in token operations
489-
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
490-
"""
491-
token_dict = self.get_token_info_from_inputs()
492-
self.update_token_info_from_outputs(token_dict)
493-
self.check_authorities_and_deposit(token_dict)
494-
495320
def iter_spent_rewards(self) -> Iterator[Block]:
496321
"""Iterate over all the rewards being spent, assumes tx has been verified."""
497322
for input_tx in self.inputs:
@@ -500,51 +325,6 @@ def iter_spent_rewards(self) -> Iterator[Block]:
500325
assert isinstance(spent_tx, Block)
501326
yield spent_tx
502327

503-
def verify_inputs(self, *, skip_script: bool = False) -> None:
504-
"""Verify inputs signatures and ownership and all inputs actually exist"""
505-
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
506-
507-
spent_outputs: set[tuple[VertexId, int]] = set()
508-
for input_tx in self.inputs:
509-
if len(input_tx.data) > self._settings.MAX_INPUT_DATA_SIZE:
510-
raise InvalidInputDataSize('size: {} and max-size: {}'.format(
511-
len(input_tx.data), self._settings.MAX_INPUT_DATA_SIZE
512-
))
513-
514-
try:
515-
spent_tx = self.get_spent_tx(input_tx)
516-
assert spent_tx.hash is not None
517-
if input_tx.index >= len(spent_tx.outputs):
518-
raise InexistentInput('Output spent by this input does not exist: {} index {}'.format(
519-
input_tx.tx_id.hex(), input_tx.index))
520-
except TransactionDoesNotExist:
521-
raise InexistentInput('Input tx does not exist: {}'.format(input_tx.tx_id.hex()))
522-
523-
if self.timestamp <= spent_tx.timestamp:
524-
raise TimestampError('tx={} timestamp={}, spent_tx={} timestamp={}'.format(
525-
self.hash.hex() if self.hash else None,
526-
self.timestamp,
527-
spent_tx.hash.hex(),
528-
spent_tx.timestamp,
529-
))
530-
531-
if not skip_script:
532-
self.verify_script(input_tx, spent_tx)
533-
534-
# check if any other input in this tx is spending the same output
535-
key = (input_tx.tx_id, input_tx.index)
536-
if key in spent_outputs:
537-
raise ConflictingInputs('tx {} inputs spend the same output: {} index {}'.format(
538-
self.hash_hex, input_tx.tx_id.hex(), input_tx.index))
539-
spent_outputs.add(key)
540-
541-
def verify_reward_locked(self) -> None:
542-
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only
543-
the block rewards spent by this tx itself, and not the inherited `min_height`."""
544-
info = self.get_spent_reward_locked_info()
545-
if info is not None:
546-
raise RewardLocked(f'Reward {info.block_hash.hex()} still needs {info.blocks_needed} to be unlocked.')
547-
548328
def is_spent_reward_locked(self) -> bool:
549329
""" Check whether any spent reward is currently locked, considering only the block rewards spent by this tx
550330
itself, and not the inherited `min_height`"""
@@ -578,17 +358,6 @@ def _spent_reward_needed_height(self, block: Block) -> int:
578358
needed_height = self._settings.REWARD_SPEND_MIN_BLOCKS - spend_blocks
579359
return max(needed_height, 0)
580360

581-
def verify_script(self, input_tx: TxInput, spent_tx: BaseTransaction) -> None:
582-
"""
583-
:type input_tx: TxInput
584-
:type spent_tx: Transaction
585-
"""
586-
from hathor.transaction.scripts import script_eval
587-
try:
588-
script_eval(self, input_tx, spent_tx)
589-
except ScriptError as e:
590-
raise InvalidInputData(e) from e
591-
592361
def is_double_spending(self) -> bool:
593362
""" Iterate through inputs to check if they were already spent
594363
Used to prevent users from sending double spending transactions to the network

hathor/verification/token_creation_transaction_verifier.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from hathor.transaction.exceptions import InvalidToken, TransactionDataError
1516
from hathor.transaction.token_creation_tx import TokenCreationTransaction
17+
from hathor.transaction.transaction import TokenInfo, Transaction
18+
from hathor.transaction.util import clean_token_string
1619
from hathor.verification.transaction_verifier import TransactionVerifier
1720

1821

@@ -27,7 +30,42 @@ def verify(self, tx: TokenCreationTransaction, *, reject_locked_reward: bool = T
2730
super().verify(tx, reject_locked_reward=reject_locked_reward)
2831
self.verify_token_info(tx)
2932

33+
def verify_sum(self, tx: Transaction) -> None:
34+
""" Besides all checks made on regular transactions, a few extra ones are made:
35+
- only HTR tokens on the inputs;
36+
- new tokens are actually being minted;
37+
38+
:raises InvalidToken: when there's an error in token operations
39+
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
40+
"""
41+
assert isinstance(tx, TokenCreationTransaction)
42+
token_dict = tx.get_token_info_from_inputs()
43+
44+
# we add the created token's info to token_dict, as the creation tx allows for mint/melt
45+
assert tx.hash is not None
46+
token_dict[tx.hash] = TokenInfo(0, True, True)
47+
48+
self.update_token_info_from_outputs(tx, token_dict=token_dict)
49+
50+
# make sure tokens are being minted
51+
token_info = token_dict[tx.hash]
52+
if token_info.amount <= 0:
53+
raise InvalidToken('Token creation transaction must mint new tokens')
54+
55+
self.verify_authorities_and_deposit(token_dict)
56+
3057
def verify_token_info(self, tx: TokenCreationTransaction) -> None:
3158
""" Validates token info
3259
"""
33-
tx.verify_token_info()
60+
name_len = len(tx.token_name)
61+
symbol_len = len(tx.token_symbol)
62+
if name_len == 0 or name_len > self._settings.MAX_LENGTH_TOKEN_NAME:
63+
raise TransactionDataError('Invalid token name length ({})'.format(name_len))
64+
if symbol_len == 0 or symbol_len > self._settings.MAX_LENGTH_TOKEN_SYMBOL:
65+
raise TransactionDataError('Invalid token symbol length ({})'.format(symbol_len))
66+
67+
# Can't create token with hathor name or symbol
68+
if clean_token_string(tx.token_name) == clean_token_string(self._settings.HATHOR_TOKEN_NAME):
69+
raise TransactionDataError('Invalid token name ({})'.format(tx.token_name))
70+
if clean_token_string(tx.token_symbol) == clean_token_string(self._settings.HATHOR_TOKEN_SYMBOL):
71+
raise TransactionDataError('Invalid token symbol ({})'.format(tx.token_symbol))

0 commit comments

Comments
 (0)