Skip to content

Commit 24de4ef

Browse files
committed
refactor(verification): move verification methods signatures
1 parent 86e908c commit 24de4ef

14 files changed

+241
-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

+5-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:
@@ -525,8 +526,10 @@ def handle_submit(self, params: dict, msgid: Optional[str]) -> None:
525526

526527
self.log.debug('share received', block=tx, block_base=block_base.hex(), block_base_hash=block_base_hash.hex())
527528

529+
verifier = VertexVerifier(settings=self._settings)
530+
528531
try:
529-
tx.verify_pow(job.weight)
532+
verifier.verify_pow(tx, override_weight=job.weight)
530533
except PowError:
531534
self.log.error('bad share, discard', job_weight=job.weight, tx=tx)
532535
return self.send_error(INVALID_SOLUTION, msgid, {
@@ -542,7 +545,7 @@ def handle_submit(self, params: dict, msgid: Optional[str]) -> None:
542545
self.manager.reactor.callLater(0, self.job_request)
543546

544547
try:
545-
tx.verify_pow()
548+
verifier.verify_pow(tx)
546549
except PowError:
547550
# Transaction pow was not enough, but the share was succesfully submited
548551
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

+31-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,35 @@ 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+
def verify_height(self, block: Block) -> None:
58+
"""Validate that the block height is enough to confirm all transactions being confirmed."""
5059
block.verify_height()
60+
61+
def verify_weight(self, block: Block) -> None:
62+
"""Validate minimum block difficulty."""
63+
block.verify_weight()
64+
65+
def verify_reward(self, block: Block) -> None:
66+
"""Validate reward amount."""
67+
block.verify_reward()
68+
69+
def verify_no_inputs(self, block: Block) -> None:
70+
block.verify_no_inputs()
71+
72+
def verify_outputs(self, block: BaseTransaction) -> None:
73+
block.verify_outputs()
74+
75+
def verify_data(self, block: Block) -> None:
76+
block.verify_data()

hathor/verification/merge_mined_block_verifier.py

+6
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
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+
def verify_aux_pow(self, block: MergeMinedBlock) -> None:
23+
""" Verify auxiliary proof-of-work (for merged mining).
24+
"""
25+
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

+80-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,80 @@ 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+
def verify_parents_basic(self, tx: Transaction) -> None:
65+
"""Verify number and non-duplicity of parents."""
66+
tx.verify_parents_basic()
67+
68+
def verify_weight(self, tx: Transaction) -> None:
69+
"""Validate minimum tx difficulty."""
70+
tx.verify_weight()
71+
72+
def verify_without_storage(self, tx: Transaction) -> None:
73+
""" Run all verifications that do not need a storage.
74+
"""
5075
tx.verify_without_storage()
76+
77+
def verify_sigops_input(self, tx: Transaction) -> None:
78+
""" Count sig operations on all inputs and verify that the total sum is below the limit
79+
"""
5180
tx.verify_sigops_input()
52-
tx.verify_inputs() # need to run verify_inputs first to check if all inputs exist
53-
tx.verify_parents()
81+
82+
def verify_inputs(self, tx: Transaction, *, skip_script: bool = False) -> None:
83+
"""Verify inputs signatures and ownership and all inputs actually exist"""
84+
tx.verify_inputs(skip_script=skip_script)
85+
86+
def verify_script(self, *, tx: Transaction, input_tx: TxInput, spent_tx: BaseTransaction) -> None:
87+
"""
88+
:type tx: Transaction
89+
:type input_tx: TxInput
90+
:type spent_tx: Transaction
91+
"""
92+
tx.verify_script(input_tx, spent_tx)
93+
94+
def verify_sum(self, tx: Transaction) -> None:
95+
"""Verify that the sum of outputs is equal of the sum of inputs, for each token.
96+
97+
If there are authority UTXOs involved, tokens can be minted or melted, so the above rule may
98+
not be respected.
99+
100+
:raises InvalidToken: when there's an error in token operations
101+
:raises InputOutputMismatch: if sum of inputs is not equal to outputs and there's no mint/melt
102+
"""
54103
tx.verify_sum()
55-
if reject_locked_reward:
56-
tx.verify_reward_locked()
104+
105+
def verify_reward_locked(self, tx: Transaction) -> None:
106+
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only
107+
the block rewards spent by this tx itself, and not the inherited `min_height`."""
108+
tx.verify_reward_locked()
109+
110+
def verify_number_of_inputs(self, tx: Transaction) -> None:
111+
"""Verify number of inputs is in a valid range"""
112+
tx.verify_number_of_inputs()
113+
114+
def verify_outputs(self, tx: BaseTransaction) -> None:
115+
"""Verify outputs reference an existing token uid in the tokens list
116+
117+
:raises InvalidToken: output references non existent token uid
118+
"""
119+
tx.verify_outputs()
120+
121+
def update_token_info_from_outputs(self, tx: Transaction, *, token_dict: dict[TokenUid, TokenInfo]) -> None:
122+
"""Iterate over the outputs and add values to token info dict. Updates the dict in-place.
123+
124+
Also, checks if no token has authorities on the outputs not present on the inputs
125+
126+
:raises InvalidToken: when there's an error in token operations
127+
"""
128+
tx.update_token_info_from_outputs(token_dict)

hathor/verification/verification_service.py

+17
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,23 @@ def verify(self, vertex: BaseTransaction, *, reject_locked_reward: bool = True)
141141
case _:
142142
raise NotImplementedError
143143

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

hathor/verification/vertex_verifier.py

+41
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,52 @@
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+
def verify_pow(self, vertex: BaseTransaction, *, override_weight: Optional[float] = None) -> None:
41+
"""Verify proof-of-work
42+
43+
:raises PowError: when the hash is equal or greater than the target
44+
"""
45+
vertex.verify_pow(override_weight)
46+
47+
def verify_outputs(self, vertex: BaseTransaction) -> None:
48+
"""Verify there are no hathor authority UTXOs and outputs are all positive
49+
50+
:raises InvalidToken: when there's a hathor authority utxo
51+
:raises InvalidOutputValue: output has negative value
52+
:raises TooManyOutputs: when there are too many outputs
53+
"""
54+
vertex.verify_outputs()
55+
56+
def verify_number_of_outputs(self, vertex: BaseTransaction) -> None:
57+
"""Verify number of outputs does not exceeds the limit"""
58+
vertex.verify_number_of_outputs()
59+
60+
def verify_sigops_output(self, vertex: BaseTransaction) -> None:
61+
""" Count sig operations on all outputs and verify that the total sum is below the limit
62+
"""
63+
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(settings=self._settings).verify_pow(tx, override_weight=0.)
1617

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

tests/tx/test_genesis.py

+7-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,21 @@ def get_genesis_output():
2628
class GenesisTest(unittest.TestCase):
2729
def setUp(self):
2830
super().setUp()
31+
verifiers = VertexVerifiers.create_defaults(settings=self._settings)
32+
self._verification_service = VerificationService(verifiers=verifiers)
2933
self.storage = TransactionMemoryStorage()
3034

3135
def test_pow(self):
36+
verifier = VertexVerifier(settings=self._settings)
3237
genesis = self.storage.get_all_genesis()
3338
for g in genesis:
3439
self.assertEqual(g.calculate_hash(), g.hash)
35-
self.assertIsNone(g.verify_pow())
40+
self.assertIsNone(verifier.verify_pow(g))
3641

3742
def test_verify(self):
3843
genesis = self.storage.get_all_genesis()
3944
for g in genesis:
40-
g.verify_without_storage()
45+
self._verification_service.verify_without_storage(g)
4146

4247
def test_output(self):
4348
# Test if block output is valid

0 commit comments

Comments
 (0)