Skip to content

Commit 13398a6

Browse files
committed
refactor(verification): move verification methods signatures
1 parent 8f2d608 commit 13398a6

14 files changed

+246
-50
lines changed

hathor/cli/mining.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
import requests
2626

27+
from hathor.conf.get_settings import get_settings
28+
from hathor.verification.block_verifier import BlockVerifier
29+
2730
_SLEEP_ON_ERROR_SECONDS = 5
2831
_MAX_CONN_RETRIES = math.inf
2932

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

136139
try:
137-
block.verify_without_storage()
140+
settings = get_settings()
141+
verifier = BlockVerifier(settings=settings)
142+
verifier.verify_without_storage(block)
138143
except HathorError:
139144
print('[{}] ERROR: Block has not been pushed because it is not valid.'.format(datetime.datetime.now()))
140145
else:

hathor/stratum/stratum.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from hathor.transaction import BaseTransaction, BitcoinAuxPow, Block, MergeMinedBlock, Transaction, sum_weights
4242
from hathor.transaction.exceptions import PowError, ScriptError, TxValidationError
4343
from hathor.util import Reactor, json_dumpb, json_loadb, reactor
44+
from hathor.verification.vertex_verifier import VertexVerifier
4445
from hathor.wallet.exceptions import InvalidAddress
4546

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

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

544545
try:
545-
tx.verify_pow()
546+
VertexVerifier.verify_pow(tx)
546547
except PowError:
547548
# Transaction pow was not enough, but the share was succesfully submited
548549
self.log.info('high hash, keep mining', tx=tx)

hathor/transaction/resources/create_tx.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def render_POST(self, request):
8989
# conservative estimate of the input data size to estimate a valid weight
9090
tx_input.data = b'\0' * 107
9191
tx.weight = minimum_tx_weight(fake_signed_tx)
92-
tx.verify_unsigned_skip_pow()
92+
self.manager.verification_service.verifiers.tx.verify_unsigned_skip_pow(tx)
9393

9494
if tx.is_double_spending():
9595
raise InvalidNewTransaction('At least one of your inputs has already been spent.')

hathor/verification/block_verifier.py

+34-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
from hathor.profiler import get_cpu_profiler
16-
from hathor.transaction import Block
16+
from hathor.transaction import BaseTransaction, Block
1717
from hathor.verification.vertex_verifier import VertexVerifier
1818

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

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

45-
block.verify_without_storage()
45+
self.verify_without_storage(block)
4646

4747
# (1) and (4)
48-
block.verify_parents()
48+
self.verify_parents(block)
49+
50+
self.verify_height(block)
51+
52+
def verify_without_storage(self, block: Block) -> None:
53+
""" Run all verifications that do not need a storage.
54+
"""
55+
block.verify_without_storage()
4956

57+
@staticmethod
58+
def verify_height(block: Block) -> None:
59+
"""Validate that the block height is enough to confirm all transactions being confirmed."""
5060
block.verify_height()
61+
62+
def verify_weight(self, block: Block) -> None:
63+
"""Validate minimum block difficulty."""
64+
block.verify_weight()
65+
66+
@staticmethod
67+
def verify_reward(block: Block) -> None:
68+
"""Validate reward amount."""
69+
block.verify_reward()
70+
71+
@staticmethod
72+
def verify_no_inputs(block: Block) -> None:
73+
block.verify_no_inputs()
74+
75+
def verify_outputs(self, block: BaseTransaction) -> None:
76+
block.verify_outputs()
77+
78+
def verify_data(self, block: Block) -> None:
79+
block.verify_data()

hathor/verification/merge_mined_block_verifier.py

+7
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from hathor.transaction import MergeMinedBlock
1516
from hathor.verification.block_verifier import BlockVerifier
1617

1718

1819
class MergeMinedBlockVerifier(BlockVerifier):
1920
__slots__ = ()
21+
22+
@staticmethod
23+
def verify_aux_pow(block: MergeMinedBlock) -> None:
24+
""" Verify auxiliary proof-of-work (for merged mining).
25+
"""
26+
block.verify_aux_pow()

hathor/verification/token_creation_transaction_verifier.py

+5
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ def verify(self, tx: TokenCreationTransaction, *, reject_locked_reward: bool = T
2525
We also overload verify_sum to make some different checks
2626
"""
2727
super().verify(tx, reject_locked_reward=reject_locked_reward)
28+
self.verify_token_info(tx)
29+
30+
def verify_token_info(self, tx: TokenCreationTransaction) -> None:
31+
""" Validates token info
32+
"""
2833
tx.verify_token_info()

hathor/verification/transaction_verifier.py

+84-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# limitations under the License.
1414

1515
from hathor.profiler import get_cpu_profiler
16-
from hathor.transaction import Transaction
16+
from hathor.transaction import BaseTransaction, Transaction, TxInput
17+
from hathor.transaction.transaction import TokenInfo
18+
from hathor.types import TokenUid
1719
from hathor.verification.vertex_verifier import VertexVerifier
1820

1921
cpu = get_cpu_profiler()
@@ -27,9 +29,9 @@ def verify_basic(self, tx: Transaction) -> None:
2729
if tx.is_genesis:
2830
# TODO do genesis validation?
2931
return
30-
tx.verify_parents_basic()
31-
tx.verify_weight()
32-
tx.verify_without_storage()
32+
self.verify_parents_basic(tx)
33+
self.verify_weight(tx)
34+
self.verify_without_storage(tx)
3335

3436
@cpu.profiler(key=lambda _, tx: 'tx-verify!{}'.format(tx.hash.hex()))
3537
def verify(self, tx: Transaction, *, reject_locked_reward: bool = True) -> None:
@@ -47,10 +49,84 @@ def verify(self, tx: Transaction, *, reject_locked_reward: bool = True) -> None:
4749
if tx.is_genesis:
4850
# TODO do genesis validation
4951
return
52+
self.verify_without_storage(tx)
53+
self.verify_sigops_input(tx)
54+
self.verify_inputs(tx) # need to run verify_inputs first to check if all inputs exist
55+
self.verify_parents(tx)
56+
self.verify_sum(tx)
57+
if reject_locked_reward:
58+
self.verify_reward_locked(tx)
59+
60+
def verify_unsigned_skip_pow(self, tx: Transaction) -> None:
61+
""" Same as .verify but skipping pow and signature verification."""
62+
tx.verify_unsigned_skip_pow()
63+
64+
@staticmethod
65+
def verify_parents_basic(tx: Transaction) -> None:
66+
"""Verify number and non-duplicity of parents."""
67+
tx.verify_parents_basic()
68+
69+
def verify_weight(self, tx: Transaction) -> None:
70+
"""Validate minimum tx difficulty."""
71+
tx.verify_weight()
72+
73+
def verify_without_storage(self, tx: Transaction) -> None:
74+
""" Run all verifications that do not need a storage.
75+
"""
5076
tx.verify_without_storage()
77+
78+
def verify_sigops_input(self, tx: Transaction) -> None:
79+
""" Count sig operations on all inputs and verify that the total sum is below the limit
80+
"""
5181
tx.verify_sigops_input()
52-
tx.verify_inputs() # need to run verify_inputs first to check if all inputs exist
53-
tx.verify_parents()
82+
83+
def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None:
84+
"""Verify inputs signatures and ownership and all inputs actually exist"""
85+
tx.verify_inputs(skip_script=skip_script)
86+
87+
@staticmethod
88+
def verify_script(*, tx: Transaction, input_tx: TxInput, spent_tx: BaseTransaction) -> None:
89+
"""
90+
:type tx: Transaction
91+
:type input_tx: TxInput
92+
:type spent_tx: Transaction
93+
"""
94+
tx.verify_script(input_tx, spent_tx)
95+
96+
def verify_sum(self, tx: Transaction) -> None:
97+
"""Verify that the sum of outputs is equal of the sum of inputs, for each token.
98+
99+
If there are authority UTXOs involved, tokens can be minted or melted, so the above rule may
100+
not be respected.
101+
102+
:raises InvalidToken: when there's an error in token operations
103+
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
104+
"""
54105
tx.verify_sum()
55-
if reject_locked_reward:
56-
tx.verify_reward_locked()
106+
107+
@staticmethod
108+
def verify_reward_locked(tx: Transaction) -> None:
109+
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only
110+
the block rewards spent by this tx itself, and not the inherited `min_height`."""
111+
tx.verify_reward_locked()
112+
113+
def verify_number_of_inputs(self, tx: Transaction) -> None:
114+
"""Verify number of inputs is in a valid range"""
115+
tx.verify_number_of_inputs()
116+
117+
def verify_outputs(self, tx: BaseTransaction) -> None:
118+
"""Verify outputs reference an existing token uid in the tokens list
119+
120+
:raises InvalidToken: output references non existent token uid
121+
"""
122+
tx.verify_outputs()
123+
124+
@staticmethod
125+
def update_token_info_from_outputs(tx: Transaction, *, token_dict: dict[TokenUid, TokenInfo]) -> None:
126+
"""Iterate over the outputs and add values to token info dict. Updates the dict in-place.
127+
128+
Also, checks if no token has authorities on the outputs not present on the inputs
129+
130+
:raises InvalidToken: when there's an error in token operations
131+
"""
132+
tx.update_token_info_from_outputs(token_dict)

hathor/verification/verification_service.py

+17
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,23 @@ def verify(self, vertex: BaseTransaction, *, reject_locked_reward: bool = True)
136136
case _:
137137
raise NotImplementedError
138138

139+
def verify_without_storage(self, vertex: BaseTransaction) -> None:
140+
match vertex.version:
141+
case TxVersion.REGULAR_BLOCK:
142+
assert isinstance(vertex, Block)
143+
self.verifiers.block.verify_without_storage(vertex)
144+
case TxVersion.MERGE_MINED_BLOCK:
145+
assert isinstance(vertex, MergeMinedBlock)
146+
self.verifiers.merge_mined_block.verify_without_storage(vertex)
147+
case TxVersion.REGULAR_TRANSACTION:
148+
assert isinstance(vertex, Transaction)
149+
self.verifiers.tx.verify_without_storage(vertex)
150+
case TxVersion.TOKEN_CREATION_TRANSACTION:
151+
assert isinstance(vertex, TokenCreationTransaction)
152+
self.verifiers.token_creation_tx.verify_without_storage(vertex)
153+
case _:
154+
raise NotImplementedError
155+
139156
def validate_vertex_error(self, vertex: BaseTransaction) -> tuple[bool, str]:
140157
""" Verify if tx is valid and return success and possible error message
141158

hathor/verification/vertex_verifier.py

+42
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,53 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Optional
16+
1517
from hathor.conf.settings import HathorSettings
18+
from hathor.transaction import BaseTransaction
1619

1720

1821
class VertexVerifier:
1922
__slots__ = ('_settings', )
2023

2124
def __init__(self, *, settings: HathorSettings):
2225
self._settings = settings
26+
27+
def verify_parents(self, vertex: BaseTransaction) -> None:
28+
"""All parents must exist and their timestamps must be smaller than ours.
29+
30+
Also, txs should have 2 other txs as parents, while blocks should have 2 txs + 1 block.
31+
32+
Parents must be ordered with blocks first, followed by transactions.
33+
34+
:raises TimestampError: when our timestamp is less or equal than our parent's timestamp
35+
:raises ParentDoesNotExist: when at least one of our parents does not exist
36+
:raises IncorrectParents: when tx does not confirm the correct number/type of parent txs
37+
"""
38+
vertex.verify_parents()
39+
40+
@classmethod
41+
def verify_pow(cls, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
42+
"""Verify proof-of-work
43+
44+
:raises PowError: when the hash is equal or greater than the target
45+
"""
46+
vertex.verify_pow(override_weight)
47+
48+
def verify_outputs(self, vertex: BaseTransaction) -> None:
49+
"""Verify there are no hathor authority UTXOs and outputs are all positive
50+
51+
:raises InvalidToken: when there's a hathor authority utxo
52+
:raises InvalidOutputValue: output has negative value
53+
:raises TooManyOutputs: when there are too many outputs
54+
"""
55+
vertex.verify_outputs()
56+
57+
def verify_number_of_outputs(self, vertex: BaseTransaction) -> None:
58+
"""Verify number of outputs does not exceeds the limit"""
59+
vertex.verify_number_of_outputs()
60+
61+
def verify_sigops_output(self, vertex: BaseTransaction) -> None:
62+
""" Count sig operations on all outputs and verify that the total sum is below the limit
63+
"""
64+
vertex.verify_sigops_output()

tests/simulation/test_simulator.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from hathor.simulator import FakeConnection
44
from hathor.simulator.trigger import All as AllTriggers, StopWhenSynced
5+
from hathor.verification.vertex_verifier import VertexVerifier
56
from tests import unittest
67
from tests.simulation.base import SimulatorTestCase
78

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

1718
def test_one_node(self):
1819
manager1 = self.create_peer()

tests/tx/test_genesis.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from hathor.conf import HathorSettings
22
from hathor.daa import TestMode, _set_test_mode, calculate_block_difficulty, minimum_tx_weight
33
from hathor.transaction.storage import TransactionMemoryStorage
4+
from hathor.verification.verification_service import VerificationService, VertexVerifiers
5+
from hathor.verification.vertex_verifier import VertexVerifier
46
from tests import unittest
57

68
settings = HathorSettings()
@@ -26,18 +28,20 @@ def get_genesis_output():
2628
class GenesisTest(unittest.TestCase):
2729
def setUp(self):
2830
super().setUp()
31+
verifiers = VertexVerifiers.create(settings=self._settings)
32+
self._verification_service = VerificationService(verifiers=verifiers)
2933
self.storage = TransactionMemoryStorage()
3034

3135
def test_pow(self):
3236
genesis = self.storage.get_all_genesis()
3337
for g in genesis:
3438
self.assertEqual(g.calculate_hash(), g.hash)
35-
self.assertIsNone(g.verify_pow())
39+
self.assertIsNone(VertexVerifier.verify_pow(g))
3640

3741
def test_verify(self):
3842
genesis = self.storage.get_all_genesis()
3943
for g in genesis:
40-
g.verify_without_storage()
44+
self._verification_service.verify_without_storage(g)
4145

4246
def test_output(self):
4347
# Test if block output is valid

0 commit comments

Comments
 (0)