Skip to content

Commit

Permalink
refactor(verification): implement VerificationDependencies and Verifi…
Browse files Browse the repository at this point in the history
…cationModel
  • Loading branch information
glevco committed May 8, 2024
1 parent 4b6a318 commit 7a6d76d
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 137 deletions.
37 changes: 21 additions & 16 deletions hathor/daa.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,17 @@

from enum import IntFlag
from math import log
from typing import TYPE_CHECKING, ClassVar, Optional
from typing import TYPE_CHECKING, Callable, ClassVar, Optional

from structlog import get_logger

from hathor.conf.settings import HathorSettings
from hathor.profiler import get_cpu_profiler
from hathor.types import VertexId
from hathor.util import iwindows, not_none
from hathor.util import iwindows

if TYPE_CHECKING:
from hathor.transaction import Block, Transaction
from hathor.transaction.storage.simple_memory_storage import SimpleMemoryStorage
from hathor.transaction.storage.vertex_storage_protocol import VertexStorageProtocol

logger = get_logger()
cpu = get_cpu_profiler()
Expand All @@ -60,35 +58,43 @@ def __init__(self, *, settings: HathorSettings, test_mode: TestMode = TestMode.D
DifficultyAdjustmentAlgorithm.singleton = self

@cpu.profiler(key=lambda _, block: 'calculate_block_difficulty!{}'.format(block.hash.hex()))
def calculate_block_difficulty(self, block: 'Block', memory_storage: 'SimpleMemoryStorage') -> float:
def calculate_block_difficulty(self, block: 'Block', parent_block_getter: Callable[['Block'], 'Block']) -> float:
""" Calculate block weight according to the ascendants of `block`, using calculate_next_weight."""
if self.TEST_MODE & TestMode.TEST_BLOCK_WEIGHT:
return 1.0

if block.is_genesis:
return self.MIN_BLOCK_WEIGHT

parent_block = memory_storage.get_parent_block(block)

return self.calculate_next_weight(parent_block, block.timestamp, memory_storage)
parent_block = parent_block_getter(block)
return self.calculate_next_weight(parent_block, block.timestamp, parent_block_getter)

def _calculate_N(self, parent_block: 'Block') -> int:
"""Calculate the N value for the `calculate_next_weight` algorithm."""
return min(2 * self._settings.BLOCK_DIFFICULTY_N_BLOCKS, parent_block.get_height() - 1)

def get_block_dependencies(self, block: 'Block') -> list[VertexId]:
def get_block_dependencies(
self,
block: 'Block',
parent_block_getter: Callable[['Block'], 'Block'],
) -> list[VertexId]:
"""Return the ids of the required blocks to call `calculate_block_difficulty` for the provided block."""
parent_block = block.get_block_parent()
parent_block = parent_block_getter(block)
N = self._calculate_N(parent_block)
ids: list[VertexId] = [not_none(parent_block.hash)]
ids: list[VertexId] = [parent_block.hash]

while len(ids) <= N + 1:
parent_block = parent_block.get_block_parent()
ids.append(not_none(parent_block.hash))
parent_block = parent_block_getter(parent_block)
ids.append(parent_block.hash)

return ids

def calculate_next_weight(self, parent_block: 'Block', timestamp: int, storage: 'VertexStorageProtocol') -> float:
def calculate_next_weight(
self,
parent_block: 'Block',
timestamp: int,
parent_block_getter: Callable[['Block'], 'Block'],
) -> float:
""" Calculate the next block weight, aka DAA/difficulty adjustment algorithm.
The algorithm used is described in [RFC 22](https://gitlab.com/HathorNetwork/rfcs/merge_requests/22).
Expand All @@ -111,8 +117,7 @@ def calculate_next_weight(self, parent_block: 'Block', timestamp: int, storage:
blocks: list['Block'] = []
while len(blocks) < N + 1:
blocks.append(root)
root = storage.get_parent_block(root)
assert root is not None
root = parent_block_getter(root)

# TODO: revise if this assertion can be safely removed
assert blocks == sorted(blocks, key=lambda tx: -tx.timestamp)
Expand Down
5 changes: 4 additions & 1 deletion hathor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,10 @@ def _make_block_template(self, parent_block: Block, parent_txs: 'ParentTxs', cur
parent_block_metadata.score,
2 * self._settings.WEIGHT_TOL
)
weight = max(self.daa.calculate_next_weight(parent_block, timestamp, self.tx_storage), min_significant_weight)
weight = max(
self.daa.calculate_next_weight(parent_block, timestamp, self.tx_storage.get_parent_block),
min_significant_weight
)
height = parent_block.get_height() + 1
parents = [parent_block.hash] + parent_txs.must_include
parents_any = parent_txs.can_include
Expand Down
2 changes: 1 addition & 1 deletion hathor/simulator/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,6 @@ def _build_vertex_verifiers(settings: HathorSettings, daa: DifficultyAdjustmentA
"""
return VertexVerifiers.create(
settings=settings,
vertex_verifier=SimulatorVertexVerifier(settings=settings, daa=daa),
vertex_verifier=SimulatorVertexVerifier(settings=settings),
daa=daa,
)
2 changes: 1 addition & 1 deletion hathor/stratum/stratum.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def handle_submit(self, params: dict, msgid: Optional[str]) -> None:

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

verifier = VertexVerifier(settings=self._settings, daa=self.manager.daa)
verifier = VertexVerifier(settings=self._settings)

try:
verifier.verify_pow(tx, override_weight=job.weight)
Expand Down
99 changes: 0 additions & 99 deletions hathor/transaction/storage/simple_memory_storage.py

This file was deleted.

16 changes: 5 additions & 11 deletions hathor/verification/block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
TransactionDataError,
WeightError,
)
from hathor.transaction.storage.simple_memory_storage import SimpleMemoryStorage
from hathor.util import not_none


Expand All @@ -40,19 +39,14 @@ def __init__(self, *, settings: HathorSettings, daa: DifficultyAdjustmentAlgorit

def verify_height(self, block: Block) -> None:
"""Validate that the block height is enough to confirm all transactions being confirmed."""
meta = block.get_metadata()
assert meta.height is not None
assert meta.min_height is not None
if meta.height < meta.min_height:
raise RewardLocked(f'Block needs {meta.min_height} height but has {meta.height}')
if block.static_metadata.height < block.static_metadata.min_height:
raise RewardLocked(
f'Block needs {block.static_metadata.min_height} height but has {block.static_metadata.height}'
)

def verify_weight(self, block: Block) -> None:
"""Validate minimum block difficulty."""
memory_storage = SimpleMemoryStorage()
dependencies = self._daa.get_block_dependencies(block)
memory_storage.add_vertices_from_storage(not_none(block.storage), dependencies)

min_block_weight = self._daa.calculate_block_difficulty(block, memory_storage)
min_block_weight = self._daa.calculate_block_difficulty(block, not_none(block.storage).get_parent_block)
if block.weight < min_block_weight - self._settings.WEIGHT_TOL:
raise WeightError(f'Invalid new block {block.hash_hex}: weight ({block.weight}) is '
f'smaller than the minimum weight ({min_block_weight})')
Expand Down
115 changes: 115 additions & 0 deletions hathor/verification/verification_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2023 Hathor Labs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dataclasses import dataclass
from typing import TYPE_CHECKING

from typing_extensions import Self

from hathor.daa import DifficultyAdjustmentAlgorithm
from hathor.reward_lock import get_spent_reward_locked_info
from hathor.reward_lock.reward_lock import get_minimum_best_height
from hathor.transaction import Block, Vertex
from hathor.transaction.transaction import RewardLockedInfo, TokenInfo, Transaction
from hathor.types import TokenUid, VertexId

if TYPE_CHECKING:
from hathor.transaction.storage import TransactionStorage


@dataclass(frozen=True, slots=True)
class VertexDependencies:
"""A dataclass of dependencies necessary for vertex verification."""
parents: dict[VertexId, Vertex]

@staticmethod
def _get_parents_from_storage(vertex: Vertex, storage: 'TransactionStorage') -> dict[VertexId, Vertex]:
return {vertex_id: storage.get_vertex(vertex_id) for vertex_id in vertex.parents}


@dataclass(frozen=True, slots=True)
class BasicBlockDependencies(VertexDependencies):
"""A dataclass of dependencies necessary for basic block verification."""
daa_deps: dict[VertexId, Block] | None

@classmethod
def create_from_storage(
cls,
block: Block,
*,
storage: 'TransactionStorage',
daa: DifficultyAdjustmentAlgorithm,
skip_weight_verification: bool,
) -> Self:
"""Create a basic block dependencies instance using dependencies from a storage."""
parents = cls._get_parents_from_storage(block, storage)
daa_deps: dict[VertexId, Block] | None = None

if not block.is_genesis and not skip_weight_verification:
daa_dep_ids = daa.get_block_dependencies(block, storage.get_parent_block)
daa_deps = {vertex_id: storage.get_block(vertex_id) for vertex_id in daa_dep_ids}

return cls(
parents=parents,
daa_deps=daa_deps,
)

def get_parent_block(self) -> Block:
"""Return the parent block of the block being verified."""
parent_blocks = [vertex for vertex in self.parents.values() if isinstance(vertex, Block)]
assert len(parent_blocks) == 1
return parent_blocks[0]

def get_parent_block_for_daa(self, block: Block) -> Block:
"""A method for getting parent blocks during DAA-related verification."""
assert self.daa_deps is not None
parent_hash = block.get_block_parent_hash()
return self.daa_deps[parent_hash]


@dataclass(frozen=True, slots=True)
class BlockDependencies(VertexDependencies):
"""A dataclass of dependencies necessary for block verification."""

@classmethod
def create_from_storage(cls, block: Block, *, storage: 'TransactionStorage') -> Self:
"""Create a block dependencies instance using dependencies from a storage."""
parents = cls._get_parents_from_storage(block, storage)
return cls(parents=parents)


@dataclass(frozen=True, slots=True)
class TransactionDependencies(VertexDependencies):
"""A dataclass of dependencies necessary for transaction verification."""
spent_txs: dict[VertexId, Vertex]
token_info: dict[TokenUid, TokenInfo]
reward_locked_info: RewardLockedInfo | None
best_block_height: int

@classmethod
def create_from_storage(cls, tx: Transaction, storage: 'TransactionStorage') -> Self:
"""Create a transaction dependencies instance using dependencies from a storage."""
parents = cls._get_parents_from_storage(tx, storage)
spent_txs = {input_tx.tx_id: storage.get_vertex(input_tx.tx_id) for input_tx in tx.inputs}
token_info = tx.get_complete_token_info()
reward_locked_info = get_spent_reward_locked_info(tx, storage)
best_block_height = get_minimum_best_height(storage)

return cls(
parents=parents,
spent_txs=spent_txs,
token_info=token_info,
reward_locked_info=reward_locked_info,
best_block_height=best_block_height,
)
Loading

0 comments on commit 7a6d76d

Please sign in to comment.