Skip to content

Commit

Permalink
refactor(verification): move verification methods signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Oct 10, 2023
1 parent 1cb8d78 commit 160989e
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 50 deletions.
7 changes: 6 additions & 1 deletion hathor/cli/mining.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@

import requests

from hathor.conf.get_settings import get_settings
from hathor.verification.block_verifier import BlockVerifier

_SLEEP_ON_ERROR_SECONDS = 5
_MAX_CONN_RETRIES = math.inf

Expand Down Expand Up @@ -134,7 +137,9 @@ def execute(args: Namespace) -> None:
block.nonce, block.weight))

try:
block.verify_without_storage()
settings = get_settings()
verifier = BlockVerifier(settings=settings)
verifier.verify_without_storage(block)
except HathorError:
print('[{}] ERROR: Block has not been pushed because it is not valid.'.format(datetime.datetime.now()))
else:
Expand Down
5 changes: 3 additions & 2 deletions hathor/stratum/stratum.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from hathor.transaction import BaseTransaction, BitcoinAuxPow, Block, MergeMinedBlock, Transaction, sum_weights
from hathor.transaction.exceptions import PowError, ScriptError, TxValidationError
from hathor.util import Reactor, json_dumpb, json_loadb, reactor
from hathor.verification.vertex_verifier import VertexVerifier
from hathor.wallet.exceptions import InvalidAddress

if TYPE_CHECKING:
Expand Down Expand Up @@ -526,7 +527,7 @@ def handle_submit(self, params: dict, msgid: Optional[str]) -> None:
self.log.debug('share received', block=tx, block_base=block_base.hex(), block_base_hash=block_base_hash.hex())

try:
tx.verify_pow(job.weight)
VertexVerifier.verify_pow(tx, override_weight=job.weight)
except PowError:
self.log.error('bad share, discard', job_weight=job.weight, tx=tx)
return self.send_error(INVALID_SOLUTION, msgid, {
Expand All @@ -542,7 +543,7 @@ def handle_submit(self, params: dict, msgid: Optional[str]) -> None:
self.manager.reactor.callLater(0, self.job_request)

try:
tx.verify_pow()
VertexVerifier.verify_pow(tx)
except PowError:
# Transaction pow was not enough, but the share was succesfully submited
self.log.info('high hash, keep mining', tx=tx)
Expand Down
2 changes: 1 addition & 1 deletion hathor/transaction/resources/create_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def render_POST(self, request):
# conservative estimate of the input data size to estimate a valid weight
tx_input.data = b'\0' * 107
tx.weight = minimum_tx_weight(fake_signed_tx)
tx.verify_unsigned_skip_pow()
self.manager.verification_service.verifiers.tx.verify_unsigned_skip_pow(tx)

if tx.is_double_spending():
raise InvalidNewTransaction('At least one of your inputs has already been spent.')
Expand Down
39 changes: 34 additions & 5 deletions hathor/verification/block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

from hathor.profiler import get_cpu_profiler
from hathor.transaction import Block
from hathor.transaction import BaseTransaction, Block
from hathor.verification.vertex_verifier import VertexVerifier

cpu = get_cpu_profiler()
Expand All @@ -25,8 +25,8 @@ class BlockVerifier(VertexVerifier):
def verify_basic(self, block: Block, *, skip_block_weight_verification: bool = False) -> None:
"""Partially run validations, the ones that need parents/inputs are skipped."""
if not skip_block_weight_verification:
block.verify_weight()
block.verify_reward()
self.verify_weight(block)
self.verify_reward(block)

@cpu.profiler(key=lambda _, block: 'block-verify!{}'.format(block.hash.hex()))
def verify(self, block: Block) -> None:
Expand All @@ -42,9 +42,38 @@ def verify(self, block: Block) -> None:
# TODO do genesis validation
return

block.verify_without_storage()
self.verify_without_storage(block)

# (1) and (4)
block.verify_parents()
self.verify_parents(block)

self.verify_height(block)

def verify_without_storage(self, block: Block) -> None:
""" Run all verifications that do not need a storage.
"""
block.verify_without_storage()

@staticmethod
def verify_height(block: Block) -> None:
"""Validate that the block height is enough to confirm all transactions being confirmed."""
block.verify_height()

def verify_weight(self, block: Block) -> None:
"""Validate minimum block difficulty."""
block.verify_weight()

@staticmethod
def verify_reward(block: Block) -> None:
"""Validate reward amount."""
block.verify_reward()

@staticmethod
def verify_no_inputs(block: Block) -> None:
block.verify_no_inputs()

def verify_outputs(self, block: BaseTransaction) -> None:
block.verify_outputs()

def verify_data(self, block: Block) -> None:
block.verify_data()
7 changes: 7 additions & 0 deletions hathor/verification/merge_mined_block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from hathor.transaction import MergeMinedBlock
from hathor.verification.block_verifier import BlockVerifier


class MergeMinedBlockVerifier(BlockVerifier):
__slots__ = ()

@staticmethod
def verify_aux_pow(block: MergeMinedBlock) -> None:
""" Verify auxiliary proof-of-work (for merged mining).
"""
block.verify_aux_pow()
5 changes: 5 additions & 0 deletions hathor/verification/token_creation_transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ def verify(self, tx: TokenCreationTransaction, *, reject_locked_reward: bool = T
We also overload verify_sum to make some different checks
"""
super().verify(tx, reject_locked_reward=reject_locked_reward)
self.verify_token_info(tx)

def verify_token_info(self, tx: TokenCreationTransaction) -> None:
""" Validates token info
"""
tx.verify_token_info()
92 changes: 84 additions & 8 deletions hathor/verification/transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# limitations under the License.

from hathor.profiler import get_cpu_profiler
from hathor.transaction import Transaction
from hathor.transaction import BaseTransaction, Transaction, TxInput
from hathor.transaction.transaction import TokenInfo
from hathor.types import TokenUid
from hathor.verification.vertex_verifier import VertexVerifier

cpu = get_cpu_profiler()
Expand All @@ -27,9 +29,9 @@ def verify_basic(self, tx: Transaction) -> None:
if tx.is_genesis:
# TODO do genesis validation?
return
tx.verify_parents_basic()
tx.verify_weight()
tx.verify_without_storage()
self.verify_parents_basic(tx)
self.verify_weight(tx)
self.verify_without_storage(tx)

@cpu.profiler(key=lambda _, tx: 'tx-verify!{}'.format(tx.hash.hex()))
def verify(self, tx: Transaction, *, reject_locked_reward: bool = True) -> None:
Expand All @@ -47,10 +49,84 @@ def verify(self, tx: Transaction, *, reject_locked_reward: bool = True) -> None:
if tx.is_genesis:
# TODO do genesis validation
return
self.verify_without_storage(tx)
self.verify_sigops_input(tx)
self.verify_inputs(tx) # need to run verify_inputs first to check if all inputs exist
self.verify_parents(tx)
self.verify_sum(tx)
if reject_locked_reward:
self.verify_reward_locked(tx)

def verify_unsigned_skip_pow(self, tx: Transaction) -> None:
""" Same as .verify but skipping pow and signature verification."""
tx.verify_unsigned_skip_pow()

@staticmethod
def verify_parents_basic(tx: Transaction) -> None:
"""Verify number and non-duplicity of parents."""
tx.verify_parents_basic()

def verify_weight(self, tx: Transaction) -> None:
"""Validate minimum tx difficulty."""
tx.verify_weight()

def verify_without_storage(self, tx: Transaction) -> None:
""" Run all verifications that do not need a storage.
"""
tx.verify_without_storage()

def verify_sigops_input(self, tx: Transaction) -> None:
""" Count sig operations on all inputs and verify that the total sum is below the limit
"""
tx.verify_sigops_input()
tx.verify_inputs() # need to run verify_inputs first to check if all inputs exist
tx.verify_parents()

def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None:
"""Verify inputs signatures and ownership and all inputs actually exist"""
tx.verify_inputs(skip_script=skip_script)

@staticmethod
def verify_script(*, tx: Transaction, input_tx: TxInput, spent_tx: BaseTransaction) -> None:
"""
:type tx: Transaction
:type input_tx: TxInput
:type spent_tx: Transaction
"""
tx.verify_script(input_tx, spent_tx)

def verify_sum(self, tx: Transaction) -> None:
"""Verify that the sum of outputs is equal of the sum of inputs, for each token.
If there are authority UTXOs involved, tokens can be minted or melted, so the above rule may
not be respected.
:raises InvalidToken: when there's an error in token operations
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
"""
tx.verify_sum()
if reject_locked_reward:
tx.verify_reward_locked()

@staticmethod
def verify_reward_locked(tx: Transaction) -> None:
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only
the block rewards spent by this tx itself, and not the inherited `min_height`."""
tx.verify_reward_locked()

def verify_number_of_inputs(self, tx: Transaction) -> None:
"""Verify number of inputs is in a valid range"""
tx.verify_number_of_inputs()

def verify_outputs(self, tx: BaseTransaction) -> None:
"""Verify outputs reference an existing token uid in the tokens list
:raises InvalidToken: output references non existent token uid
"""
tx.verify_outputs()

@staticmethod
def update_token_info_from_outputs(tx: Transaction, *, token_dict: dict[TokenUid, TokenInfo]) -> None:
"""Iterate over the outputs and add values to token info dict. Updates the dict in-place.
Also, checks if no token has authorities on the outputs not present on the inputs
:raises InvalidToken: when there's an error in token operations
"""
tx.update_token_info_from_outputs(token_dict)
17 changes: 17 additions & 0 deletions hathor/verification/verification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ def verify(self, vertex: BaseTransaction, *, reject_locked_reward: bool = True)
case _:
raise NotImplementedError

def verify_without_storage(self, vertex: BaseTransaction) -> None:
match vertex.version:
case TxVersion.REGULAR_BLOCK:
assert isinstance(vertex, Block)
self.verifiers.block.verify_without_storage(vertex)
case TxVersion.MERGE_MINED_BLOCK:
assert isinstance(vertex, MergeMinedBlock)
self.verifiers.merge_mined_block.verify_without_storage(vertex)
case TxVersion.REGULAR_TRANSACTION:
assert isinstance(vertex, Transaction)
self.verifiers.tx.verify_without_storage(vertex)
case TxVersion.TOKEN_CREATION_TRANSACTION:
assert isinstance(vertex, TokenCreationTransaction)
self.verifiers.token_creation_tx.verify_without_storage(vertex)
case _:
raise NotImplementedError

def validate_vertex_error(self, vertex: BaseTransaction) -> tuple[bool, str]:
""" Verify if tx is valid and return success and possible error message
Expand Down
42 changes: 42 additions & 0 deletions hathor/verification/vertex_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,53 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional

from hathor.conf.settings import HathorSettings
from hathor.transaction import BaseTransaction


class VertexVerifier:
__slots__ = ('_settings', )

def __init__(self, *, settings: HathorSettings):
self._settings = settings

def verify_parents(self, vertex: BaseTransaction) -> 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
"""
vertex.verify_parents()

@classmethod
def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
"""Verify proof-of-work
:raises PowError: when the hash is equal or greater than the target
"""
vertex.verify_pow(override_weight)

def verify_outputs(self, vertex: BaseTransaction) -> 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
"""
vertex.verify_outputs()

def verify_number_of_outputs(self, vertex: BaseTransaction) -> None:
"""Verify number of outputs does not exceeds the limit"""
vertex.verify_number_of_outputs()

def verify_sigops_output(self, vertex: BaseTransaction) -> None:
""" Count sig operations on all outputs and verify that the total sum is below the limit
"""
vertex.verify_sigops_output()
3 changes: 2 additions & 1 deletion tests/simulation/test_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from hathor.simulator import FakeConnection
from hathor.simulator.trigger import All as AllTriggers, StopWhenSynced
from hathor.verification.vertex_verifier import VertexVerifier
from tests import unittest
from tests.simulation.base import SimulatorTestCase

Expand All @@ -12,7 +13,7 @@ def test_verify_pow(self):
# just get one of the genesis, we don't really need to create any transaction
tx = next(iter(manager1.tx_storage.get_all_genesis()))
# optional argument must be valid, it just has to not raise any exception, there's no assert for that
tx.verify_pow(0.)
VertexVerifier.verify_pow(tx, override_weight=0.)

def test_one_node(self):
manager1 = self.create_peer()
Expand Down
8 changes: 6 additions & 2 deletions tests/tx/test_genesis.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from hathor.conf import HathorSettings
from hathor.daa import TestMode, _set_test_mode, calculate_block_difficulty, minimum_tx_weight
from hathor.transaction.storage import TransactionMemoryStorage
from hathor.verification.verification_service import VerificationService, VertexVerifiers
from hathor.verification.vertex_verifier import VertexVerifier
from tests import unittest

settings = HathorSettings()
Expand All @@ -26,18 +28,20 @@ def get_genesis_output():
class GenesisTest(unittest.TestCase):
def setUp(self):
super().setUp()
verifiers = VertexVerifiers.create_defaults(settings=self._settings)
self._verification_service = VerificationService(verifiers=verifiers)
self.storage = TransactionMemoryStorage()

def test_pow(self):
genesis = self.storage.get_all_genesis()
for g in genesis:
self.assertEqual(g.calculate_hash(), g.hash)
self.assertIsNone(g.verify_pow())
self.assertIsNone(VertexVerifier.verify_pow(g))

def test_verify(self):
genesis = self.storage.get_all_genesis()
for g in genesis:
g.verify_without_storage()
self._verification_service.verify_without_storage(g)

def test_output(self):
# Test if block output is valid
Expand Down
Loading

0 comments on commit 160989e

Please sign in to comment.