Skip to content

refactor(verification): finish moving all verification methods [part 5/5] #800

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
Oct 25, 2023
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
24 changes: 15 additions & 9 deletions hathor/simulator/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@
from hathor.simulator.clock import HeapClock, MemoryReactorHeapClock
from hathor.simulator.miner.geometric_miner import GeometricMiner
from hathor.simulator.tx_generator import RandomTransactionGenerator
from hathor.simulator.verification import (
SimulatorBlockVerifier,
SimulatorMergeMinedBlockVerifier,
SimulatorTokenCreationTransactionVerifier,
SimulatorTransactionVerifier,
)
from hathor.util import Random
from hathor.verification.verification_service import VertexVerifiers
from hathor.wallet import HDWallet

if TYPE_CHECKING:
Expand All @@ -52,25 +59,17 @@ def _apply_patches(cls):

Patches:

- disable pow verification
- disable Transaction.resolve method
- set DAA test-mode to DISABLED (will actually run the pow function, that won't actually verify the pow)
- override AVG_TIME_BETWEEN_BLOCKS to 64
"""
from hathor.transaction import BaseTransaction

def verify_pow(self: BaseTransaction, *args: Any, **kwargs: Any) -> None:
assert self.hash is not None
logger.new().debug('Skipping BaseTransaction.verify_pow() for simulator')

def resolve(self: BaseTransaction, update_time: bool = True) -> bool:
self.update_hash()
logger.new().debug('Skipping BaseTransaction.resolve() for simulator')
return True

cls._original_verify_pow = BaseTransaction.verify_pow
BaseTransaction.verify_pow = verify_pow

cls._original_resolve = BaseTransaction.resolve
BaseTransaction.resolve = resolve

Expand All @@ -85,7 +84,6 @@ def _remove_patches(cls):
""" Remove the patches previously applied.
"""
from hathor.transaction import BaseTransaction
BaseTransaction.verify_pow = cls._original_verify_pow
BaseTransaction.resolve = cls._original_resolve

from hathor import daa
Expand Down Expand Up @@ -170,10 +168,18 @@ def create_artifacts(self, builder: Optional[Builder] = None) -> BuildArtifacts:
wallet = HDWallet(gap_limit=2)
wallet._manually_initialize()

vertex_verifiers = VertexVerifiers(
block=SimulatorBlockVerifier(settings=self.settings),
merge_mined_block=SimulatorMergeMinedBlockVerifier(settings=self.settings),
tx=SimulatorTransactionVerifier(settings=self.settings),
token_creation_tx=SimulatorTokenCreationTransactionVerifier(settings=self.settings),
)

artifacts = builder \
.set_reactor(self._clock) \
.set_rng(Random(self.rng.getrandbits(64))) \
.set_wallet(wallet) \
.set_vertex_verifiers(vertex_verifiers) \
.build()

artifacts.manager.start()
Expand Down
54 changes: 54 additions & 0 deletions hathor/simulator/verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2023 Hathor Labs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional

from structlog import get_logger

from hathor.transaction import BaseTransaction
from hathor.verification.block_verifier import BlockVerifier
from hathor.verification.merge_mined_block_verifier import MergeMinedBlockVerifier
from hathor.verification.token_creation_transaction_verifier import TokenCreationTransactionVerifier
from hathor.verification.transaction_verifier import TransactionVerifier

logger = get_logger()


def verify_pow(vertex: BaseTransaction) -> None:
assert vertex.hash is not None
logger.new().debug('Skipping BaseTransaction.verify_pow() for simulator')


class SimulatorBlockVerifier(BlockVerifier):
@classmethod
def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
verify_pow(vertex)


class SimulatorMergeMinedBlockVerifier(MergeMinedBlockVerifier):
@classmethod
def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
verify_pow(vertex)


class SimulatorTransactionVerifier(TransactionVerifier):
@classmethod
def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
verify_pow(vertex)


class SimulatorTokenCreationTransactionVerifier(TokenCreationTransactionVerifier):
@classmethod
def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
verify_pow(vertex)
153 changes: 1 addition & 152 deletions hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,7 @@

from hathor.checkpoint import Checkpoint
from hathor.conf.get_settings import get_settings
from hathor.transaction.exceptions import (
DuplicatedParents,
IncorrectParents,
InvalidOutputScriptSize,
InvalidOutputValue,
InvalidToken,
ParentDoesNotExist,
PowError,
TimestampError,
TooManyOutputs,
TooManySigOps,
WeightError,
)
from hathor.transaction.exceptions import InvalidOutputValue, WeightError
from hathor.transaction.transaction_metadata import TransactionMetadata
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
from hathor.transaction.validation_state import ValidationState
Expand Down Expand Up @@ -70,14 +58,6 @@
# Weight (d), timestamp (I), and parents len (B)
_GRAPH_FORMAT_STRING = '!dIB'

# tx should have 2 parents, both other transactions
_TX_PARENTS_TXS = 2
_TX_PARENTS_BLOCKS = 0

# blocks have 3 parents, 2 txs and 1 block
_BLOCK_PARENTS_TXS = 2
_BLOCK_PARENTS_BLOCKS = 1

# The int value of one byte
_ONE_BYTE = 0xFF

Expand Down Expand Up @@ -540,137 +520,6 @@ def verify_checkpoint(self, checkpoints: list[Checkpoint]) -> None:
To be implemented by tx/block, used by `self.validate_checkpoint`. Should not modify the validation state."""
raise NotImplementedError

def verify_parents(self) -> None:
"""All parents must exist and their timestamps must be smaller than ours.

Also, txs should have 2 other txs as parents, while blocks should have 2 txs + 1 block.

Parents must be ordered with blocks first, followed by transactions.

:raises TimestampError: when our timestamp is less or equal than our parent's timestamp
:raises ParentDoesNotExist: when at least one of our parents does not exist
:raises IncorrectParents: when tx does not confirm the correct number/type of parent txs
"""
from hathor.transaction.storage.exceptions import TransactionDoesNotExist

assert self.storage is not None

# check if parents are duplicated
parents_set = set(self.parents)
if len(self.parents) > len(parents_set):
raise DuplicatedParents('Tx has duplicated parents: {}', [tx_hash.hex() for tx_hash in self.parents])

my_parents_txs = 0 # number of tx parents
my_parents_blocks = 0 # number of block parents
min_timestamp: Optional[int] = None

for parent_hash in self.parents:
try:
parent = self.storage.get_transaction(parent_hash)
assert parent.hash is not None
if self.timestamp <= parent.timestamp:
raise TimestampError('tx={} timestamp={}, parent={} timestamp={}'.format(
self.hash_hex,
self.timestamp,
parent.hash_hex,
parent.timestamp,
))

if parent.is_block:
if self.is_block and not parent.is_genesis:
if self.timestamp - parent.timestamp > self._settings.MAX_DISTANCE_BETWEEN_BLOCKS:
raise TimestampError('Distance between blocks is too big'
' ({} seconds)'.format(self.timestamp - parent.timestamp))
if my_parents_txs > 0:
raise IncorrectParents('Parents which are blocks must come before transactions')
for pi_hash in parent.parents:
pi = self.storage.get_transaction(parent_hash)
if not pi.is_block:
min_timestamp = (
min(min_timestamp, pi.timestamp) if min_timestamp is not None
else pi.timestamp
)
my_parents_blocks += 1
else:
if min_timestamp and parent.timestamp < min_timestamp:
raise TimestampError('tx={} timestamp={}, parent={} timestamp={}, min_timestamp={}'.format(
self.hash_hex,
self.timestamp,
parent.hash_hex,
parent.timestamp,
min_timestamp
))
my_parents_txs += 1
except TransactionDoesNotExist:
raise ParentDoesNotExist('tx={} parent={}'.format(self.hash_hex, parent_hash.hex()))

# check for correct number of parents
if self.is_block:
parents_txs = _BLOCK_PARENTS_TXS
parents_blocks = _BLOCK_PARENTS_BLOCKS
else:
parents_txs = _TX_PARENTS_TXS
parents_blocks = _TX_PARENTS_BLOCKS
if my_parents_blocks != parents_blocks:
raise IncorrectParents('wrong number of parents (block type): {}, expecting {}'.format(
my_parents_blocks, parents_blocks))
if my_parents_txs != parents_txs:
raise IncorrectParents('wrong number of parents (tx type): {}, expecting {}'.format(
my_parents_txs, parents_txs))

def verify_pow(self, override_weight: Optional[float] = None) -> None:
"""Verify proof-of-work

:raises PowError: when the hash is equal or greater than the target
"""
assert self.hash is not None
numeric_hash = int(self.hash_hex, self.HEX_BASE)
minimum_target = self.get_target(override_weight)
if numeric_hash >= minimum_target:
raise PowError(f'Transaction has invalid data ({numeric_hash} < {minimum_target})')

def verify_number_of_outputs(self) -> None:
"""Verify number of outputs does not exceeds the limit"""
if len(self.outputs) > self._settings.MAX_NUM_OUTPUTS:
raise TooManyOutputs('Maximum number of outputs exceeded')

def verify_sigops_output(self) -> None:
""" Count sig operations on all outputs and verify that the total sum is below the limit
"""
from hathor.transaction.scripts import get_sigops_count
n_txops = 0

for tx_output in self.outputs:
n_txops += get_sigops_count(tx_output.script)

if n_txops > self._settings.MAX_TX_SIGOPS_OUTPUT:
raise TooManySigOps('TX[{}]: Maximum number of sigops for all outputs exceeded ({})'.format(
self.hash_hex, n_txops))

def verify_outputs(self) -> None:
"""Verify there are no hathor authority UTXOs and outputs are all positive

:raises InvalidToken: when there's a hathor authority utxo
:raises InvalidOutputValue: output has negative value
:raises TooManyOutputs: when there are too many outputs
"""
self.verify_number_of_outputs()
for index, output in enumerate(self.outputs):
# no hathor authority UTXO
if (output.get_token_index() == 0) and output.is_token_authority():
raise InvalidToken('Cannot have authority UTXO for hathor tokens: {}'.format(
output.to_human_readable()))

# output value must be positive
if output.value <= 0:
raise InvalidOutputValue('Output value must be a positive integer. Value: {} and index: {}'.format(
output.value, index))

if len(output.script) > self._settings.MAX_OUTPUT_SCRIPT_SIZE:
raise InvalidOutputScriptSize('size: {} and max-size: {}'.format(
len(output.script), self._settings.MAX_OUTPUT_SCRIPT_SIZE
))

def resolve(self, update_time: bool = False) -> bool:
"""Run a CPU mining looking for the nonce that solves the proof-of-work

Expand Down
8 changes: 1 addition & 7 deletions hathor/transaction/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.profiler import get_cpu_profiler
from hathor.transaction import BaseTransaction, TxOutput, TxVersion
from hathor.transaction.exceptions import BlockWithTokensError, CheckpointError
from hathor.transaction.exceptions import CheckpointError
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
from hathor.util import not_none
from hathor.utils.int import get_bit_list
Expand Down Expand Up @@ -328,12 +328,6 @@ def verify_checkpoint(self, checkpoints: list[Checkpoint]) -> None:
# TODO: check whether self is a parent of any checkpoint-valid block, this is left for a future PR
pass

def verify_outputs(self) -> None:
super().verify_outputs()
for output in self.outputs:
if output.get_token_index() > 0:
raise BlockWithTokensError('in output: {}'.format(output.to_human_readable()))

def get_base_hash(self) -> bytes:
from hathor.merged_mining.bitcoin import sha256d_hash
return sha256d_hash(self.get_header_without_nonce())
Expand Down
12 changes: 0 additions & 12 deletions hathor/transaction/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from hathor.profiler import get_cpu_profiler
from hathor.transaction import BaseTransaction, Block, TxInput, TxOutput, TxVersion
from hathor.transaction.base_transaction import TX_HASH_SIZE
from hathor.transaction.exceptions import 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 @@ -279,17 +278,6 @@ def verify_checkpoint(self, checkpoints: list[Checkpoint]) -> None:
raise InvalidNewTransaction(f'Invalid new transaction {self.hash_hex}: expected to reach a checkpoint but '
'none of its children is checkpoint-valid')

def verify_outputs(self) -> None:
"""Verify outputs reference an existing token uid in the tokens list
:raises InvalidToken: output references non existent token uid
"""
super().verify_outputs()
for output in self.outputs:
# check index is valid
if output.get_token_index() > len(self.tokens):
raise InvalidToken('token uid index not available: index {}'.format(output.get_token_index()))

def get_token_info_from_inputs(self) -> dict[TokenUid, TokenInfo]:
"""Sum up all tokens present in the inputs and their properties (amount, can_mint, can_melt)
"""
Expand Down
7 changes: 6 additions & 1 deletion hathor/verification/block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from hathor.transaction import BaseTransaction, Block
from hathor.transaction.exceptions import (
BlockWithInputs,
BlockWithTokensError,
InvalidBlockReward,
RewardLocked,
TransactionDataError,
Expand Down Expand Up @@ -97,7 +98,11 @@ def verify_no_inputs(self, block: Block) -> None:
raise BlockWithInputs('number of inputs {}'.format(len(inputs)))

def verify_outputs(self, block: BaseTransaction) -> None:
block.verify_outputs()
assert isinstance(block, Block)
super().verify_outputs(block)
for output in block.outputs:
if output.get_token_index() > 0:
raise BlockWithTokensError('in output: {}'.format(output.to_human_readable()))

def verify_data(self, block: Block) -> None:
if len(block.data) > self._settings.BLOCK_DATA_MAX_SIZE:
Expand Down
7 changes: 6 additions & 1 deletion hathor/verification/transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,12 @@ def verify_outputs(self, tx: BaseTransaction) -> None:

:raises InvalidToken: output references non existent token uid
"""
tx.verify_outputs()
assert isinstance(tx, Transaction)
super().verify_outputs(tx)
for output in tx.outputs:
# check index is valid
if output.get_token_index() > len(tx.tokens):
raise InvalidToken('token uid index not available: index {}'.format(output.get_token_index()))

def verify_authorities_and_deposit(self, token_dict: dict[TokenUid, TokenInfo]) -> None:
"""Verify that the sum of outputs is equal of the sum of inputs, for each token. If sum of inputs
Expand Down
Loading