Skip to content

Commit 7eb1d79

Browse files
committed
refactor(daa): externalize block dependencies
1 parent 74b7251 commit 7eb1d79

File tree

5 files changed

+135
-9
lines changed

5 files changed

+135
-9
lines changed

hathor/daa.py

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

2828
from hathor.conf.settings import HathorSettings
2929
from hathor.profiler import get_cpu_profiler
30+
from hathor.types import VertexId
3031
from hathor.util import iwindows, not_none
3132

3233
if TYPE_CHECKING:
3334
from hathor.transaction import Block, Transaction
35+
from hathor.transaction.storage.simple_memory_storage import SimpleMemoryStorage
3436
from hathor.transaction.storage.vertex_storage_protocol import VertexStorageProtocol
3537

3638
logger = get_logger()
@@ -58,15 +60,33 @@ def __init__(self, *, settings: HathorSettings, test_mode: TestMode = TestMode.D
5860
DifficultyAdjustmentAlgorithm.singleton = self
5961

6062
@cpu.profiler(key=lambda _, block: 'calculate_block_difficulty!{}'.format(block.hash.hex()))
61-
def calculate_block_difficulty(self, block: 'Block') -> float:
62-
""" Calculate block weight according to the ascendents of `block`, using calculate_next_weight."""
63+
def calculate_block_difficulty(self, block: 'Block', memory_storage: 'SimpleMemoryStorage') -> float:
64+
""" Calculate block weight according to the ascendants of `block`, using calculate_next_weight."""
6365
if self.TEST_MODE & TestMode.TEST_BLOCK_WEIGHT:
6466
return 1.0
6567

6668
if block.is_genesis:
6769
return self.MIN_BLOCK_WEIGHT
6870

69-
return self.calculate_next_weight(block.get_block_parent(), block.timestamp, not_none(block.storage))
71+
parent_block = memory_storage.get_parent_block(block)
72+
73+
return self.calculate_next_weight(parent_block, block.timestamp, memory_storage)
74+
75+
def _calculate_N(self, parent_block: 'Block') -> int:
76+
"""Calculate the N value for the `calculate_next_weight` algorithm."""
77+
return min(2 * self._settings.BLOCK_DIFFICULTY_N_BLOCKS, parent_block.get_height() - 1)
78+
79+
def get_block_dependencies(self, block: 'Block') -> list[VertexId]:
80+
"""Return the ids of the required blocks to call `calculate_block_difficulty` for the provided block."""
81+
parent_block = block.get_block_parent()
82+
N = self._calculate_N(parent_block)
83+
ids: list[VertexId] = [not_none(parent_block.hash)]
84+
85+
while len(ids) <= N + 1:
86+
parent_block = parent_block.get_block_parent()
87+
ids.append(not_none(parent_block.hash))
88+
89+
return ids
7090

7191
def calculate_next_weight(self, parent_block: 'Block', timestamp: int, storage: 'VertexStorageProtocol') -> float:
7292
""" Calculate the next block weight, aka DAA/difficulty adjustment algorithm.
@@ -81,7 +101,7 @@ def calculate_next_weight(self, parent_block: 'Block', timestamp: int, storage:
81101
from hathor.transaction import sum_weights
82102

83103
root = parent_block
84-
N = min(2 * self._settings.BLOCK_DIFFICULTY_N_BLOCKS, parent_block.get_height() - 1)
104+
N = self._calculate_N(parent_block)
85105
K = N // 2
86106
T = self.AVG_TIME_BETWEEN_BLOCKS
87107
S = 5

hathor/transaction/base_transaction.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,7 @@ def serialize_output(tx: BaseTransaction, tx_out: TxOutput) -> dict[str, Any]:
842842

843843
return ret
844844

845-
def clone(self, *, include_metadata: bool = True) -> 'BaseTransaction':
845+
def clone(self, *, include_metadata: bool = True, include_storage: bool = True) -> 'BaseTransaction':
846846
"""Return exact copy without sharing memory, including metadata if loaded.
847847
848848
:return: Transaction or Block copy
@@ -851,7 +851,8 @@ def clone(self, *, include_metadata: bool = True) -> 'BaseTransaction':
851851
if hasattr(self, '_metadata') and include_metadata:
852852
assert self._metadata is not None # FIXME: is this actually true or do we have to check if not None
853853
new_tx._metadata = self._metadata.clone()
854-
new_tx.storage = self.storage
854+
if include_storage:
855+
new_tx.storage = self.storage
855856
return new_tx
856857

857858
@abstractmethod
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright 2023 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.transaction import Block, Transaction
16+
from hathor.transaction.base_transaction import BaseTransaction
17+
from hathor.transaction.storage import TransactionStorage
18+
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
19+
from hathor.types import VertexId
20+
21+
22+
class SimpleMemoryStorage:
23+
"""
24+
Instances of this class simply facilitate storing some data in memory, specifically for pre-fetched verification
25+
dependencies.
26+
"""
27+
__slots__ = ('_blocks', '_transactions',)
28+
29+
def __init__(self) -> None:
30+
self._blocks: dict[VertexId, BaseTransaction] = {}
31+
self._transactions: dict[VertexId, BaseTransaction] = {}
32+
33+
@property
34+
def _vertices(self) -> dict[VertexId, BaseTransaction]:
35+
"""Blocks and Transactions together."""
36+
return {**self._blocks, **self._transactions}
37+
38+
def get_block(self, block_id: VertexId) -> Block:
39+
"""Return a block from the storage, throw if it's not found."""
40+
block = self._get_vertex(self._blocks, block_id)
41+
assert isinstance(block, Block)
42+
return block
43+
44+
def get_transaction(self, tx_id: VertexId) -> Transaction:
45+
"""Return a transaction from the storage, throw if it's not found."""
46+
tx = self._get_vertex(self._transactions, tx_id)
47+
assert isinstance(tx, Transaction)
48+
return tx
49+
50+
@staticmethod
51+
def _get_vertex(storage: dict[VertexId, BaseTransaction], vertex_id: VertexId) -> BaseTransaction:
52+
"""Return a vertex from a storage, throw if it's not found."""
53+
if vertex := storage.get(vertex_id):
54+
return vertex
55+
56+
raise TransactionDoesNotExist(f'Vertex "{vertex_id.hex()}" does not exist in this SimpleMemoryStorage.')
57+
58+
def get_parent_block(self, block: Block) -> Block:
59+
"""Get the parent block of a block."""
60+
parent_hash = block.get_block_parent_hash()
61+
62+
return self.get_block(parent_hash)
63+
64+
def add_vertices_from_storage(self, storage: TransactionStorage, ids: list[VertexId]) -> None:
65+
"""
66+
Add multiple vertices to this storage. It automatically fetches data from the provided TransactionStorage
67+
and a list of ids.
68+
"""
69+
for vertex_id in ids:
70+
self.add_vertex_from_storage(storage, vertex_id)
71+
72+
def add_vertex_from_storage(self, storage: TransactionStorage, vertex_id: VertexId) -> None:
73+
"""
74+
Add a vertex to this storage. It automatically fetches data from the provided TransactionStorage and a list
75+
of ids.
76+
"""
77+
if vertex_id in self._vertices:
78+
return
79+
80+
vertex = storage.get_transaction(vertex_id)
81+
clone = vertex.clone(include_metadata=True, include_storage=False)
82+
83+
if isinstance(vertex, Block):
84+
self._blocks[vertex_id] = clone
85+
return
86+
87+
if isinstance(vertex, Transaction):
88+
self._transactions[vertex_id] = clone
89+
return
90+
91+
raise NotImplementedError
92+
93+
def get_vertex(self, vertex_id: VertexId) -> BaseTransaction:
94+
# TODO: Currently unused, will be implemented in a next PR.
95+
raise NotImplementedError
96+
97+
def get_best_block_tips(self) -> list[VertexId]:
98+
# TODO: Currently unused, will be implemented in a next PR.
99+
raise NotImplementedError

hathor/verification/block_verifier.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
TransactionDataError,
2626
WeightError,
2727
)
28+
from hathor.transaction.storage.simple_memory_storage import SimpleMemoryStorage
29+
from hathor.util import not_none
2830

2931

3032
class BlockVerifier:
@@ -51,7 +53,11 @@ def verify_height(self, block: Block) -> None:
5153

5254
def verify_weight(self, block: Block) -> None:
5355
"""Validate minimum block difficulty."""
54-
min_block_weight = self._daa.calculate_block_difficulty(block)
56+
memory_storage = SimpleMemoryStorage()
57+
dependencies = self._daa.get_block_dependencies(block)
58+
memory_storage.add_vertices_from_storage(not_none(block.storage), dependencies)
59+
60+
min_block_weight = self._daa.calculate_block_difficulty(block, memory_storage)
5561
if block.weight < min_block_weight - self._settings.WEIGHT_TOL:
5662
raise WeightError(f'Invalid new block {block.hash_hex}: weight ({block.weight}) is '
5763
f'smaller than the minimum weight ({min_block_weight})')

tests/tx/test_genesis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ def test_genesis_weight(self):
7474
# Validate the block and tx weight
7575
# in test mode weight is always 1
7676
self._daa.TEST_MODE = TestMode.TEST_ALL_WEIGHT
77-
self.assertEqual(self._daa.calculate_block_difficulty(genesis_block), 1)
77+
self.assertEqual(self._daa.calculate_block_difficulty(genesis_block, Mock()), 1)
7878
self.assertEqual(self._daa.minimum_tx_weight(genesis_tx), 1)
7979

8080
self._daa.TEST_MODE = TestMode.DISABLED
81-
self.assertEqual(self._daa.calculate_block_difficulty(genesis_block), genesis_block.weight)
81+
self.assertEqual(self._daa.calculate_block_difficulty(genesis_block, Mock()), genesis_block.weight)
8282
self.assertEqual(self._daa.minimum_tx_weight(genesis_tx), genesis_tx.weight)

0 commit comments

Comments
 (0)