Skip to content

Commit

Permalink
refactor(verification): externalize verification dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Dec 22, 2023
1 parent 3e70c8c commit 774ccf4
Show file tree
Hide file tree
Showing 20 changed files with 345 additions and 130 deletions.
7 changes: 6 additions & 1 deletion hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,13 @@ def _get_or_create_bit_signaling_service(self) -> BitSignalingService:
def _get_or_create_verification_service(self) -> VerificationService:
if self._verification_service is None:
verifiers = self._get_or_create_vertex_verifiers()
daa = self._get_or_create_daa()
feature_service = self._get_or_create_feature_service()
self._verification_service = VerificationService(verifiers=verifiers, feature_service=feature_service)
self._verification_service = VerificationService(
verifiers=verifiers,
daa=daa,
feature_service=feature_service
)

return self._verification_service

Expand Down
6 changes: 5 additions & 1 deletion hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,11 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
daa = DifficultyAdjustmentAlgorithm(settings=settings, test_mode=test_mode)

vertex_verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa)
verification_service = VerificationService(verifiers=vertex_verifiers, feature_service=self.feature_service)
verification_service = VerificationService(
verifiers=vertex_verifiers,
daa=daa,
feature_service=self.feature_service
)

cpu_mining_service = CpuMiningService()

Expand Down
2 changes: 1 addition & 1 deletion hathor/cli/mining.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def execute(args: Namespace) -> None:
settings = get_settings()
daa = DifficultyAdjustmentAlgorithm(settings=settings)
verifiers = VertexVerifiers.create_defaults(settings=settings, daa=daa)
verification_service = VerificationService(verifiers=verifiers)
verification_service = VerificationService(verifiers=verifiers, daa=daa)
verification_service.verify_without_storage(block)
except HathorError:
print('[{}] ERROR: Block has not been pushed because it is not valid.'.format(datetime.datetime.now()))
Expand Down
11 changes: 9 additions & 2 deletions hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from _hashlib import HASH

from hathor.transaction.storage import TransactionStorage # noqa: F401
from hathor.transaction.vertex import Vertex

logger = get_logger()

Expand Down Expand Up @@ -825,7 +826,7 @@ def serialize_output(tx: BaseTransaction, tx_out: TxOutput) -> dict[str, Any]:

return ret

def clone(self, *, include_metadata: bool = True) -> 'BaseTransaction':
def clone(self, *, include_metadata: bool = True, include_storage: bool = True) -> 'BaseTransaction':
"""Return exact copy without sharing memory, including metadata if loaded.
:return: Transaction or Block copy
Expand All @@ -834,7 +835,8 @@ def clone(self, *, include_metadata: bool = True) -> 'BaseTransaction':
if hasattr(self, '_metadata') and include_metadata:
assert self._metadata is not None # FIXME: is this actually true or do we have to check if not None
new_tx._metadata = self._metadata.clone()
new_tx.storage = self.storage
if include_storage:
new_tx.storage = self.storage
return new_tx

@abstractmethod
Expand All @@ -852,6 +854,11 @@ def is_ready_for_validation(self) -> bool:
return False
return True

@abstractmethod
def as_vertex(self) -> 'Vertex':
"""Return this BaseTransaction as a Vertex."""
raise NotImplementedError


class TxInput:
_tx: BaseTransaction # XXX: used for caching on hathor.transaction.Transaction.get_spent_tx
Expand Down
8 changes: 8 additions & 0 deletions hathor/transaction/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from struct import pack
from typing import TYPE_CHECKING, Any, Optional

from typing_extensions import override

from hathor.checkpoint import Checkpoint
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_state import FeatureState
Expand All @@ -30,6 +32,7 @@

if TYPE_CHECKING:
from hathor.transaction.storage import TransactionStorage # noqa: F401
from hathor.transaction.vertex import Vertex

cpu = get_cpu_profiler()

Expand Down Expand Up @@ -401,3 +404,8 @@ def get_feature_activation_bit_value(self, bit: int) -> int:
bit_list = self._get_feature_activation_bit_list()

return bit_list[bit]

@override
def as_vertex(self) -> 'Vertex':
from hathor.transaction.vertex import BlockType
return BlockType(self)
8 changes: 8 additions & 0 deletions hathor/transaction/merge_mined_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@

from typing import TYPE_CHECKING, Any, Optional

from typing_extensions import override

from hathor.transaction.aux_pow import BitcoinAuxPow
from hathor.transaction.base_transaction import TxOutput, TxVersion
from hathor.transaction.block import Block
from hathor.transaction.util import VerboseCallback

if TYPE_CHECKING:
from hathor.transaction.storage import TransactionStorage # noqa: F401
from hathor.transaction.vertex import Vertex


class MergeMinedBlock(Block):
Expand Down Expand Up @@ -74,3 +77,8 @@ def to_json(self, decode_script: bool = False, include_metadata: bool = False) -
del json['nonce']
json['aux_pow'] = bytes(self.aux_pow).hex() if self.aux_pow else None
return json

@override
def as_vertex(self) -> 'Vertex':
from hathor.transaction.vertex import MergeMinedBlockType
return MergeMinedBlockType(self)
8 changes: 5 additions & 3 deletions hathor/transaction/resources/create_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from hathor.transaction import Transaction, TxInput, TxOutput
from hathor.transaction.scripts import create_output_script
from hathor.util import api_catch_exceptions, json_dumpb, json_loadb
from hathor.verification.verification_model import TransactionDependencies


def from_raw_output(raw_output: dict, tokens: list[bytes]) -> TxOutput:
Expand Down Expand Up @@ -109,15 +110,16 @@ def _verify_unsigned_skip_pow(self, tx: Transaction) -> None:
""" Same as .verify but skipping pow and signature verification."""
assert type(tx) is Transaction
verifiers = self.manager.verification_service.verifiers
deps = TransactionDependencies.create(tx)
verifiers.tx.verify_number_of_inputs(tx)
verifiers.vertex.verify_number_of_outputs(tx)
verifiers.vertex.verify_outputs(tx)
verifiers.tx.verify_output_token_indexes(tx)
verifiers.vertex.verify_sigops_output(tx)
verifiers.tx.verify_sigops_input(tx)
verifiers.tx.verify_sigops_input(tx, deps)
# need to run verify_inputs first to check if all inputs exist
verifiers.tx.verify_inputs(tx, skip_script=True)
verifiers.vertex.verify_parents(tx)
verifiers.tx.verify_inputs(tx, deps, skip_script=True)
verifiers.vertex.verify_parents(tx, deps)
verifiers.tx.verify_sum(tx.get_complete_token_info())


Expand Down
4 changes: 4 additions & 0 deletions hathor/transaction/storage/simple_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def get_transaction(self, tx_id: VertexId) -> Transaction:
assert isinstance(tx, Transaction)
return tx

def get_vertex(self, vertex_id: VertexId) -> BaseTransaction:
"""Return a vertex from the storage, throw if it's not found."""
return self._get_record(self._vertices, vertex_id)

@staticmethod
def _get_record(storage: dict[VertexId, _SimpleMemoryRecord], vertex_id: VertexId) -> BaseTransaction:
"""Return a record from a storage, throw if it's not found."""
Expand Down
10 changes: 9 additions & 1 deletion hathor/transaction/token_creation_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

from struct import error as StructError, pack
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional

from typing_extensions import override

Expand All @@ -23,6 +23,9 @@
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
from hathor.types import TokenUid

if TYPE_CHECKING:
from hathor.transaction.vertex import Vertex

# Signal bits (B), version (B), inputs len (B), outputs len (B)
_FUNDS_FORMAT_STRING = '!BBBB'

Expand Down Expand Up @@ -226,6 +229,11 @@ def _get_token_info_from_inputs(self) -> dict[TokenUid, TokenInfo]:

return token_dict

@override
def as_vertex(self) -> 'Vertex':
from hathor.transaction.vertex import TokenCreationTransactionType
return TokenCreationTransactionType(self)


def decode_string_utf8(encoded: bytes, key: str) -> str:
""" Raises StructError in case it's not a valid utf-8 string
Expand Down
8 changes: 8 additions & 0 deletions hathor/transaction/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from struct import pack
from typing import TYPE_CHECKING, Any, Iterator, NamedTuple, Optional

from typing_extensions import override

from hathor.checkpoint import Checkpoint
from hathor.exception import InvalidNewTransaction
from hathor.profiler import get_cpu_profiler
Expand All @@ -29,6 +31,7 @@

if TYPE_CHECKING:
from hathor.transaction.storage import TransactionStorage # noqa: F401
from hathor.transaction.vertex import Vertex

cpu = get_cpu_profiler()

Expand Down Expand Up @@ -415,3 +418,8 @@ def is_spending_voided_tx(self) -> bool:
if meta.voided_by:
return True
return False

@override
def as_vertex(self) -> 'Vertex':
from hathor.transaction.vertex import TransactionType
return TransactionType(self)
56 changes: 56 additions & 0 deletions hathor/transaction/vertex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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 dataclasses import dataclass
from typing import Generic, TypeAlias, TypeVar

from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction
from hathor.transaction.token_creation_tx import TokenCreationTransaction

T = TypeVar('T', bound=BaseTransaction)


@dataclass(frozen=True, slots=True)
class _VertexWrapper(Generic[T]):
"""
The model used for vertex verification. Includes the vertex itself and the respective necessary dependencies.
It is generic over the vertex type and dependencies type, and then reified for each one of the existing types.
"""
base_tx: T


class BlockType(_VertexWrapper[Block]):
"""Vertex verification model reified for Block."""


class MergeMinedBlockType(_VertexWrapper[MergeMinedBlock]):
"""Vertex verification model reified for MergeMinedBlock."""


class TransactionType(_VertexWrapper[Transaction]):
"""Vertex verification model reified for Transaction."""


class TokenCreationTransactionType(_VertexWrapper[TokenCreationTransaction]):
"""Vertex verification model reified for TokenCreationTransaction."""


"""
A Vertex algebraic sum type that unifies all vertex types by using a `NewType` for each one, which introduces almost no
runtime overhead.
This is useful when dealing with vertices in functional code, for example when using `match` statements, so we don't
have problems with inheritance in `case` branches.
"""
Vertex: TypeAlias = BlockType | MergeMinedBlockType | TransactionType | TokenCreationTransactionType
22 changes: 8 additions & 14 deletions hathor/verification/block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from hathor.feature_activation.feature_service import (
BlockIsMissingSignal,
BlockIsSignaling,
BlockSignalingState,
FeatureActivationIsDisabled,
)
from hathor.transaction import Block
Expand All @@ -32,8 +31,7 @@
TransactionDataError,
WeightError,
)
from hathor.transaction.storage.simple_memory_storage import SimpleMemoryStorage
from hathor.util import not_none
from hathor.verification.verification_model import BlockDependencies


class BlockVerifier:
Expand All @@ -56,20 +54,16 @@ def verify_height(self, block: Block) -> None:
if meta.height < meta.min_height:
raise RewardLocked(f'Block needs {meta.min_height} height but has {meta.height}')

def verify_weight(self, block: Block) -> None:
def verify_weight(self, block: Block, deps: BlockDependencies) -> None:
"""Validate minimum block difficulty."""
memory_storage = SimpleMemoryStorage()
dependencies = self._daa.get_block_dependencies(block)
memory_storage.add_vertices_from_storage(not_none(block.storage), dependencies)

min_block_weight = self._daa.calculate_block_difficulty(block, memory_storage)
min_block_weight = self._daa.calculate_block_difficulty(block, deps.storage)
if block.weight < min_block_weight - self._settings.WEIGHT_TOL:
raise WeightError(f'Invalid new block {block.hash_hex}: weight ({block.weight}) is '
f'smaller than the minimum weight ({min_block_weight})')

def verify_reward(self, block: Block) -> None:
def verify_reward(self, block: Block, deps: BlockDependencies) -> None:
"""Validate reward amount."""
parent_block = block.get_block_parent()
parent_block = deps.storage.get_parent_block(block)
tokens_issued_per_block = self._daa.get_tokens_issued_per_block(parent_block.get_height() + 1)
if block.sum_outputs != tokens_issued_per_block:
raise InvalidBlockReward(
Expand All @@ -91,14 +85,14 @@ def verify_data(self, block: Block) -> None:
if len(block.data) > self._settings.BLOCK_DATA_MAX_SIZE:
raise TransactionDataError('block data has {} bytes'.format(len(block.data)))

def verify_mandatory_signaling(self, signaling_state: BlockSignalingState) -> None:
def verify_mandatory_signaling(self, deps: BlockDependencies) -> None:
"""Verify whether this block is missing mandatory signaling for any feature."""
match signaling_state:
match deps.signaling_state:
case FeatureActivationIsDisabled() | BlockIsSignaling():
return
case BlockIsMissingSignal(feature):
raise BlockMustSignalError(
f"Block must signal support for feature '{feature.value}' during MUST_SIGNAL phase."
)
case _:
assert_never(signaling_state)
assert_never(deps.signaling_state)
11 changes: 5 additions & 6 deletions hathor/verification/transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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.verification.verification_model import TransactionDependencies

cpu = get_cpu_profiler()

Expand All @@ -49,8 +50,6 @@ def __init__(self, *, settings: HathorSettings, daa: DifficultyAdjustmentAlgorit

def verify_parents_basic(self, tx: Transaction) -> None:
"""Verify number and non-duplicity of parents."""
assert tx.storage is not None

# check if parents are duplicated
parents_set = set(tx.parents)
if len(tx.parents) > len(parents_set):
Expand All @@ -70,15 +69,15 @@ def verify_weight(self, tx: Transaction) -> None:
raise WeightError(f'Invalid new tx {tx.hash_hex}: weight ({tx.weight}) is '
f'greater than the maximum allowed ({max_tx_weight})')

def verify_sigops_input(self, tx: Transaction) -> None:
def verify_sigops_input(self, tx: Transaction, deps: TransactionDependencies) -> None:
""" Count sig operations on all inputs and verify that the total sum is below the limit
"""
from hathor.transaction.scripts import get_sigops_count
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
n_txops = 0
for tx_input in tx.inputs:
try:
spent_tx = tx.get_spent_tx(tx_input)
spent_tx = deps.storage.get_vertex(tx_input.tx_id)
except TransactionDoesNotExist:
raise InexistentInput('Input tx does not exist: {}'.format(tx_input.tx_id.hex()))
assert spent_tx.hash is not None
Expand All @@ -91,7 +90,7 @@ def verify_sigops_input(self, tx: Transaction) -> None:
raise TooManySigOps(
'TX[{}]: Max number of sigops for inputs exceeded ({})'.format(tx.hash_hex, n_txops))

def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None:
def verify_inputs(self, tx: Transaction, deps: TransactionDependencies, *, skip_script: bool = False) -> None:
"""Verify inputs signatures and ownership and all inputs actually exist"""
from hathor.transaction.storage.exceptions import TransactionDoesNotExist

Expand All @@ -103,7 +102,7 @@ def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None:
))

try:
spent_tx = tx.get_spent_tx(input_tx)
spent_tx = deps.storage.get_vertex(input_tx.tx_id)
assert spent_tx.hash is not None
if input_tx.index >= len(spent_tx.outputs):
raise InexistentInput('Output spent by this input does not exist: {} index {}'.format(
Expand Down
Loading

0 comments on commit 774ccf4

Please sign in to comment.