Skip to content

Commit 9d042eb

Browse files
authored
refactor(storage): create StorageProtocol (#922)
1 parent a653335 commit 9d042eb

File tree

8 files changed

+165
-52
lines changed

8 files changed

+165
-52
lines changed

hathor/daa.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727

2828
from hathor.conf.settings import HathorSettings
2929
from hathor.profiler import get_cpu_profiler
30-
from hathor.util import iwindows
30+
from hathor.util import iwindows, not_none
3131

3232
if TYPE_CHECKING:
3333
from hathor.transaction import Block, Transaction
34+
from hathor.transaction.storage.vertex_storage_protocol import VertexStorageProtocol
3435

3536
logger = get_logger()
3637
cpu = get_cpu_profiler()
@@ -65,9 +66,9 @@ def calculate_block_difficulty(self, block: 'Block') -> float:
6566
if block.is_genesis:
6667
return self.MIN_BLOCK_WEIGHT
6768

68-
return self.calculate_next_weight(block.get_block_parent(), block.timestamp)
69+
return self.calculate_next_weight(block.get_block_parent(), block.timestamp, not_none(block.storage))
6970

70-
def calculate_next_weight(self, parent_block: 'Block', timestamp: int) -> float:
71+
def calculate_next_weight(self, parent_block: 'Block', timestamp: int, storage: 'VertexStorageProtocol') -> float:
7172
""" Calculate the next block weight, aka DAA/difficulty adjustment algorithm.
7273
7374
The algorithm used is described in [RFC 22](https://gitlab.com/HathorNetwork/rfcs/merge_requests/22).
@@ -90,7 +91,7 @@ def calculate_next_weight(self, parent_block: 'Block', timestamp: int) -> float:
9091
blocks: list['Block'] = []
9192
while len(blocks) < N + 1:
9293
blocks.append(root)
93-
root = root.get_block_parent()
94+
root = storage.get_parent_block(root)
9495
assert root is not None
9596

9697
# TODO: revise if this assertion can be safely removed

hathor/manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from hathor.profiler import get_cpu_profiler
5252
from hathor.pubsub import HathorEvents, PubSubManager
5353
from hathor.reactor import ReactorProtocol as Reactor
54+
from hathor.reward_lock import is_spent_reward_locked
5455
from hathor.stratum import StratumFactory
5556
from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction, TxVersion, sum_weights
5657
from hathor.transaction.exceptions import TxValidationError
@@ -802,7 +803,7 @@ def _make_block_template(self, parent_block: Block, parent_txs: 'ParentTxs', cur
802803
parent_block_metadata.score,
803804
2 * self._settings.WEIGHT_TOL
804805
)
805-
weight = max(self.daa.calculate_next_weight(parent_block, timestamp), min_significant_weight)
806+
weight = max(self.daa.calculate_next_weight(parent_block, timestamp, self.tx_storage), min_significant_weight)
806807
height = parent_block.get_height() + 1
807808
parents = [parent_block.hash] + parent_txs.must_include
808809
parents_any = parent_txs.can_include
@@ -889,8 +890,7 @@ def push_tx(self, tx: Transaction, allow_non_standard_script: bool = False,
889890
if is_spending_voided_tx:
890891
raise SpendingVoidedError('Invalid transaction. At least one input is voided.')
891892

892-
is_spent_reward_locked = tx.is_spent_reward_locked()
893-
if is_spent_reward_locked:
893+
if is_spent_reward_locked(tx):
894894
raise RewardLockedError('Spent reward is locked.')
895895

896896
# We are using here the method from lib because the property

hathor/reward_lock/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from hathor.reward_lock.reward_lock import get_spent_reward_locked_info, is_spent_reward_locked, iter_spent_rewards
16+
17+
__all__ = [
18+
'iter_spent_rewards',
19+
'is_spent_reward_locked',
20+
'get_spent_reward_locked_info',
21+
]

hathor/reward_lock/reward_lock.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import TYPE_CHECKING, Iterator, Optional
16+
17+
from hathor.conf.get_settings import get_global_settings
18+
from hathor.transaction import Block
19+
from hathor.util import not_none
20+
21+
if TYPE_CHECKING:
22+
from hathor.transaction.storage.vertex_storage_protocol import VertexStorageProtocol
23+
from hathor.transaction.transaction import RewardLockedInfo, Transaction
24+
25+
26+
def iter_spent_rewards(tx: 'Transaction', storage: 'VertexStorageProtocol') -> Iterator[Block]:
27+
"""Iterate over all the rewards being spent, assumes tx has been verified."""
28+
for input_tx in tx.inputs:
29+
spent_tx = storage.get_vertex(input_tx.tx_id)
30+
if spent_tx.is_block:
31+
assert isinstance(spent_tx, Block)
32+
yield spent_tx
33+
34+
35+
def is_spent_reward_locked(tx: 'Transaction') -> bool:
36+
""" Check whether any spent reward is currently locked, considering only the block rewards spent by this tx
37+
itself, and not the inherited `min_height`"""
38+
return get_spent_reward_locked_info(tx, not_none(tx.storage)) is not None
39+
40+
41+
def get_spent_reward_locked_info(tx: 'Transaction', storage: 'VertexStorageProtocol') -> Optional['RewardLockedInfo']:
42+
"""Check if any input block reward is locked, returning the locked information if any, or None if they are all
43+
unlocked."""
44+
from hathor.transaction.transaction import RewardLockedInfo
45+
for blk in iter_spent_rewards(tx, storage):
46+
assert blk.hash is not None
47+
needed_height = _spent_reward_needed_height(blk, storage)
48+
if needed_height > 0:
49+
return RewardLockedInfo(blk.hash, needed_height)
50+
return None
51+
52+
53+
def _spent_reward_needed_height(block: Block, storage: 'VertexStorageProtocol') -> int:
54+
""" Returns height still needed to unlock this `block` reward: 0 means it's unlocked."""
55+
import math
56+
57+
# omitting timestamp to get the current best block, this will usually hit the cache instead of being slow
58+
tips = storage.get_best_block_tips()
59+
assert len(tips) > 0
60+
best_height = math.inf
61+
for tip in tips:
62+
blk = storage.get_block(tip)
63+
best_height = min(best_height, blk.get_height())
64+
assert isinstance(best_height, int)
65+
spent_height = block.get_height()
66+
spend_blocks = best_height - spent_height
67+
settings = get_global_settings()
68+
needed_height = settings.REWARD_SPEND_MIN_BLOCKS - spend_blocks
69+
return max(needed_height, 0)

hathor/transaction/storage/transaction_storage.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from hathor.transaction.storage.tx_allow_scope import TxAllowScope, tx_allow_context
4646
from hathor.transaction.transaction import Transaction
4747
from hathor.transaction.transaction_metadata import TransactionMetadata
48+
from hathor.types import VertexId
4849
from hathor.util import not_none
4950

5051
cpu = get_cpu_profiler()
@@ -1137,6 +1138,17 @@ def _construct_genesis_tx2(self) -> Transaction:
11371138
assert tx2.hash == self._settings.GENESIS_TX2_HASH
11381139
return tx2
11391140

1141+
def get_parent_block(self, block: Block) -> Block:
1142+
return block.get_block_parent()
1143+
1144+
def get_vertex(self, vertex_id: VertexId) -> BaseTransaction:
1145+
return self.get_transaction(vertex_id)
1146+
1147+
def get_block(self, block_id: VertexId) -> Block:
1148+
block = self.get_vertex(block_id)
1149+
assert isinstance(block, Block)
1150+
return block
1151+
11401152

11411153
class BaseTransactionStorage(TransactionStorage):
11421154
indexes: Optional[IndexesManager]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from abc import abstractmethod
16+
from typing import Protocol
17+
18+
from hathor.transaction import BaseTransaction, Block
19+
from hathor.types import VertexId
20+
21+
22+
class VertexStorageProtocol(Protocol):
23+
"""
24+
This Protocol currently represents a subset of TransactionStorage methods. Its main use case is for verification
25+
methods that can receive a RocksDB storage or an ephemeral simple memory storage.
26+
27+
Therefore, objects returned by this protocol may or may not have an `object.storage` pointer set.
28+
"""
29+
30+
@abstractmethod
31+
def get_vertex(self, vertex_id: VertexId) -> BaseTransaction:
32+
"""Return a vertex from the storage."""
33+
raise NotImplementedError
34+
35+
@abstractmethod
36+
def get_block(self, block_id: VertexId) -> Block:
37+
"""Return a block from the storage."""
38+
raise NotImplementedError
39+
40+
@abstractmethod
41+
def get_parent_block(self, block: Block) -> Block:
42+
"""Get the parent block of a block."""
43+
raise NotImplementedError
44+
45+
@abstractmethod
46+
def get_best_block_tips(self) -> list[VertexId]:
47+
"""Return a list of blocks that are heads in a best chain."""
48+
raise NotImplementedError

hathor/transaction/transaction.py

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
import hashlib
1616
from itertools import chain
1717
from struct import pack
18-
from typing import TYPE_CHECKING, Any, Iterator, NamedTuple, Optional
18+
from typing import TYPE_CHECKING, Any, NamedTuple, Optional
1919

2020
from hathor.checkpoint import Checkpoint
2121
from hathor.exception import InvalidNewTransaction
2222
from hathor.profiler import get_cpu_profiler
23-
from hathor.transaction import BaseTransaction, Block, TxInput, TxOutput, TxVersion
23+
from hathor.reward_lock import iter_spent_rewards
24+
from hathor.transaction import BaseTransaction, TxInput, TxOutput, TxVersion
2425
from hathor.transaction.base_transaction import TX_HASH_SIZE
2526
from hathor.transaction.exceptions import InvalidToken
2627
from hathor.transaction.util import VerboseCallback, unpack, unpack_len
@@ -135,7 +136,7 @@ def _calculate_inherited_min_height(self) -> int:
135136
def _calculate_my_min_height(self) -> int:
136137
""" Calculates min height derived from own spent block rewards"""
137138
min_height = 0
138-
for blk in self.iter_spent_rewards():
139+
for blk in iter_spent_rewards(self, not_none(self.storage)):
139140
min_height = max(min_height, blk.get_height() + self._settings.REWARD_SPEND_MIN_BLOCKS + 1)
140141
return min_height
141142

@@ -346,47 +347,6 @@ def _update_token_info_from_outputs(self, *, token_dict: dict[TokenUid, TokenInf
346347
sum_tokens = token_info.amount + tx_output.value
347348
token_dict[token_uid] = TokenInfo(sum_tokens, token_info.can_mint, token_info.can_melt)
348349

349-
def iter_spent_rewards(self) -> Iterator[Block]:
350-
"""Iterate over all the rewards being spent, assumes tx has been verified."""
351-
for input_tx in self.inputs:
352-
spent_tx = self.get_spent_tx(input_tx)
353-
if spent_tx.is_block:
354-
assert isinstance(spent_tx, Block)
355-
yield spent_tx
356-
357-
def is_spent_reward_locked(self) -> bool:
358-
""" Check whether any spent reward is currently locked, considering only the block rewards spent by this tx
359-
itself, and not the inherited `min_height`"""
360-
return self.get_spent_reward_locked_info() is not None
361-
362-
def get_spent_reward_locked_info(self) -> Optional[RewardLockedInfo]:
363-
"""Check if any input block reward is locked, returning the locked information if any, or None if they are all
364-
unlocked."""
365-
for blk in self.iter_spent_rewards():
366-
assert blk.hash is not None
367-
needed_height = self._spent_reward_needed_height(blk)
368-
if needed_height > 0:
369-
return RewardLockedInfo(blk.hash, needed_height)
370-
return None
371-
372-
def _spent_reward_needed_height(self, block: Block) -> int:
373-
""" Returns height still needed to unlock this `block` reward: 0 means it's unlocked."""
374-
import math
375-
assert self.storage is not None
376-
# omitting timestamp to get the current best block, this will usually hit the cache instead of being slow
377-
tips = self.storage.get_best_block_tips()
378-
assert len(tips) > 0
379-
best_height = math.inf
380-
for tip in tips:
381-
blk = self.storage.get_transaction(tip)
382-
assert isinstance(blk, Block)
383-
best_height = min(best_height, blk.get_height())
384-
assert isinstance(best_height, int)
385-
spent_height = block.get_height()
386-
spend_blocks = best_height - spent_height
387-
needed_height = self._settings.REWARD_SPEND_MIN_BLOCKS - spend_blocks
388-
return max(needed_height, 0)
389-
390350
def is_double_spending(self) -> bool:
391351
""" Iterate through inputs to check if they were already spent
392352
Used to prevent users from sending double spending transactions to the network

hathor/verification/transaction_verifier.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from hathor.conf.settings import HathorSettings
1616
from hathor.daa import DifficultyAdjustmentAlgorithm
1717
from hathor.profiler import get_cpu_profiler
18+
from hathor.reward_lock import get_spent_reward_locked_info
1819
from hathor.transaction import BaseTransaction, Transaction, TxInput
1920
from hathor.transaction.exceptions import (
2021
ConflictingInputs,
@@ -36,6 +37,7 @@
3637
from hathor.transaction.transaction import TokenInfo
3738
from hathor.transaction.util import get_deposit_amount, get_withdraw_amount
3839
from hathor.types import TokenUid, VertexId
40+
from hathor.util import not_none
3941

4042
cpu = get_cpu_profiler()
4143

@@ -144,7 +146,7 @@ def verify_script(self, *, tx: Transaction, input_tx: TxInput, spent_tx: BaseTra
144146
def verify_reward_locked(self, tx: Transaction) -> None:
145147
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering only
146148
the block rewards spent by this tx itself, and not the inherited `min_height`."""
147-
info = tx.get_spent_reward_locked_info()
149+
info = get_spent_reward_locked_info(tx, not_none(tx.storage))
148150
if info is not None:
149151
raise RewardLocked(f'Reward {info.block_hash.hex()} still needs {info.blocks_needed} to be unlocked.')
150152

0 commit comments

Comments
 (0)