Skip to content

Commit 289059e

Browse files
committed
refactor(daa): externalize block dependencies
1 parent 7eab19b commit 289059e

File tree

4 files changed

+148
-10
lines changed

4 files changed

+148
-10
lines changed

hathor/daa.py

Lines changed: 32 additions & 7 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.util import iwindows
30+
from hathor.types import VertexId
31+
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

3537
logger = get_logger()
3638
cpu = get_cpu_profiler()
@@ -57,17 +59,40 @@ def __init__(self, *, settings: HathorSettings, test_mode: TestMode = TestMode.D
5759
DifficultyAdjustmentAlgorithm.singleton = self
5860

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

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

68-
return self.calculate_next_weight(block.get_block_parent(), block.timestamp)
70+
parent_block = memory_storage.get_parent_block(block)
6971

70-
def calculate_next_weight(self, parent_block: 'Block', timestamp: int) -> float:
72+
return self.calculate_next_weight(parent_block, block.timestamp, memory_storage)
73+
74+
def _calculate_N(self, parent_block: 'Block') -> int:
75+
"""Calculate the N value for the `calculate_next_weight` algorithm."""
76+
return min(2 * self._settings.BLOCK_DIFFICULTY_N_BLOCKS, parent_block.get_height() - 1)
77+
78+
def get_block_dependencies(self, block: 'Block') -> list[VertexId]:
79+
"""Return the ids of the required blocks to call `calculate_block_difficulty` for the provided block."""
80+
parent_block = block.get_block_parent()
81+
N = self._calculate_N(parent_block)
82+
ids: list[VertexId] = [not_none(parent_block.hash)]
83+
84+
while len(ids) <= N + 1:
85+
parent_block = parent_block.get_block_parent()
86+
ids.append(not_none(parent_block.hash))
87+
88+
return ids
89+
90+
def calculate_next_weight(
91+
self,
92+
parent_block: 'Block',
93+
timestamp: int,
94+
memory_storage: Optional['SimpleMemoryStorage'] = None,
95+
) -> float:
7196
""" Calculate the next block weight, aka DAA/difficulty adjustment algorithm.
7297
7398
The algorithm used is described in [RFC 22](https://gitlab.com/HathorNetwork/rfcs/merge_requests/22).
@@ -80,7 +105,7 @@ def calculate_next_weight(self, parent_block: 'Block', timestamp: int) -> float:
80105
from hathor.transaction import sum_weights
81106

82107
root = parent_block
83-
N = min(2 * self._settings.BLOCK_DIFFICULTY_N_BLOCKS, parent_block.get_height() - 1)
108+
N = self._calculate_N(parent_block)
84109
K = N // 2
85110
T = self.AVG_TIME_BETWEEN_BLOCKS
86111
S = 5
@@ -90,7 +115,7 @@ def calculate_next_weight(self, parent_block: 'Block', timestamp: int) -> float:
90115
blocks: list['Block'] = []
91116
while len(blocks) < N + 1:
92117
blocks.append(root)
93-
root = root.get_block_parent()
118+
root = memory_storage.get_parent_block(root) if memory_storage else root.get_block_parent()
94119
assert root is not None
95120

96121
# TODO: revise if this assertion can be safely removed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 dataclasses import dataclass
16+
from typing import Any
17+
18+
from hathor.transaction import Block, Transaction, TransactionMetadata
19+
from hathor.transaction.base_transaction import BaseTransaction, tx_or_block_from_bytes
20+
from hathor.transaction.storage import TransactionStorage
21+
from hathor.transaction.storage.exceptions import TransactionDoesNotExist
22+
from hathor.types import VertexId
23+
24+
25+
@dataclass(frozen=True, slots=True)
26+
class _SimpleMemoryRecord:
27+
vertex_bytes: bytes
28+
vertex_metadata: dict[str, Any]
29+
30+
31+
class SimpleMemoryStorage:
32+
"""
33+
Instances of this class simply facilitate storing some data in memory, specifically for pre-fetched verification
34+
dependencies.
35+
"""
36+
__slots__ = ('_blocks', '_transactions',)
37+
38+
def __init__(self) -> None:
39+
self._blocks: dict[VertexId, _SimpleMemoryRecord] = {}
40+
self._transactions: dict[VertexId, _SimpleMemoryRecord] = {}
41+
42+
@property
43+
def _vertices(self) -> dict[VertexId, _SimpleMemoryRecord]:
44+
"""Blocks and Transactions together."""
45+
return {**self._blocks, **self._transactions}
46+
47+
def get_block(self, block_id: VertexId) -> Block:
48+
"""Return a block from the storage, throw if it's not found."""
49+
block = self._get_record(self._blocks, block_id)
50+
assert isinstance(block, Block)
51+
return block
52+
53+
def get_transaction(self, tx_id: VertexId) -> Transaction:
54+
"""Return a transaction from the storage, throw if it's not found."""
55+
tx = self._get_record(self._transactions, tx_id)
56+
assert isinstance(tx, Transaction)
57+
return tx
58+
59+
@staticmethod
60+
def _get_record(storage: dict[VertexId, _SimpleMemoryRecord], vertex_id: VertexId) -> BaseTransaction:
61+
"""Return a record from a storage, throw if it's not found."""
62+
if record := storage.get(vertex_id):
63+
vertex = tx_or_block_from_bytes(record.vertex_bytes)
64+
metadata = TransactionMetadata.create_from_json(record.vertex_metadata)
65+
vertex._metadata = metadata
66+
return vertex
67+
68+
raise TransactionDoesNotExist(f'Vertex "{vertex_id.hex()}" does not exist in this SimpleMemoryStorage.')
69+
70+
def get_parent_block(self, block: Block) -> Block:
71+
"""Get the parent block of a block."""
72+
parent_hash = block.get_block_parent_hash()
73+
74+
return self.get_block(parent_hash)
75+
76+
def add_vertices_from_storage(self, storage: TransactionStorage, ids: list[VertexId]) -> None:
77+
"""
78+
Add multiple vertices to this storage. It automatically fetches data from the provided TransactionStorage
79+
and a list of ids.
80+
"""
81+
for vertex_id in ids:
82+
self.add_vertex_from_storage(storage, vertex_id)
83+
84+
def add_vertex_from_storage(self, storage: TransactionStorage, vertex_id: VertexId) -> None:
85+
"""
86+
Add a vertex to this storage. It automatically fetches data from the provided TransactionStorage and a list
87+
of ids.
88+
"""
89+
if vertex_id in self._vertices:
90+
return
91+
92+
vertex = storage.get_transaction(vertex_id)
93+
vertex_bytes = vertex.get_struct()
94+
metadata = vertex.get_metadata().to_json()
95+
record = _SimpleMemoryRecord(vertex_bytes, metadata)
96+
97+
if isinstance(vertex, Block):
98+
self._blocks[vertex_id] = record
99+
return
100+
101+
if isinstance(vertex, Transaction):
102+
self._transactions[vertex_id] = record
103+
return
104+
105+
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest.mock import Mock
2+
13
from hathor.conf import HathorSettings
24
from hathor.daa import DifficultyAdjustmentAlgorithm, TestMode
35
from hathor.transaction.storage import TransactionMemoryStorage
@@ -72,9 +74,9 @@ def test_genesis_weight(self):
7274
# Validate the block and tx weight
7375
# in test mode weight is always 1
7476
self._daa.TEST_MODE = TestMode.TEST_ALL_WEIGHT
75-
self.assertEqual(self._daa.calculate_block_difficulty(genesis_block), 1)
77+
self.assertEqual(self._daa.calculate_block_difficulty(genesis_block, Mock()), 1)
7678
self.assertEqual(self._daa.minimum_tx_weight(genesis_tx), 1)
7779

7880
self._daa.TEST_MODE = TestMode.DISABLED
79-
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)
8082
self.assertEqual(self._daa.minimum_tx_weight(genesis_tx), genesis_tx.weight)

0 commit comments

Comments
 (0)