Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,8 @@ def _get_or_create_bit_signaling_service(self) -> BitSignalingService:
def _get_or_create_verification_service(self) -> VerificationService:
if self._verification_service is None:
verifiers = self._get_or_create_vertex_verifiers()
self._verification_service = VerificationService(verifiers=verifiers)
storage = self._get_or_create_tx_storage()
self._verification_service = VerificationService(verifiers=verifiers, tx_storage=storage)

return self._verification_service

Expand Down
2 changes: 1 addition & 1 deletion hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
daa=daa,
feature_service=self.feature_service
)
verification_service = VerificationService(verifiers=vertex_verifiers)
verification_service = VerificationService(verifiers=vertex_verifiers, tx_storage=tx_storage)

cpu_mining_service = CpuMiningService()

Expand Down
1 change: 1 addition & 0 deletions hathor/event/model/event_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def from_event_arguments(cls, args: EventArguments) -> 'TxData':
tx_extra_data_json = get_tx_extra_data(args.tx, detail_tokens=False)
tx_json = tx_extra_data_json['tx']
meta_json = tx_extra_data_json['meta']
meta_json['height'] = meta_json.get('height', 0) # TODO: Improve event model to reflect new static metadata
tx_json['metadata'] = meta_json
tx_json['outputs'] = [
output | dict(decoded=output['decoded'] or None)
Expand Down
23 changes: 12 additions & 11 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState
Return whether a block is signaling features that are mandatory, that is, any feature currently in the
MUST_SIGNAL phase.
"""
bit_counts = block.get_feature_activation_bit_counts()
height = block.get_height()
bit_counts = block.static_metadata.feature_activation_bit_counts
height = block.static_metadata.height
offset_to_boundary = height % self._feature_settings.evaluation_interval
remaining_blocks = self._feature_settings.evaluation_interval - offset_to_boundary - 1
descriptions = self.get_bits_description(block=block)
Expand Down Expand Up @@ -95,7 +95,7 @@ def get_state(self, *, block: 'Block', feature: Feature) -> FeatureState:
# All blocks within the same evaluation interval have the same state, that is, the state is only defined for
# the block in each interval boundary. Therefore, we get the state of the previous boundary block or calculate
# a new state if this block is a boundary block.
height = block.get_height()
height = block.static_metadata.height
offset_to_boundary = height % self._feature_settings.evaluation_interval
offset_to_previous_boundary = offset_to_boundary or self._feature_settings.evaluation_interval
previous_boundary_height = height - offset_to_previous_boundary
Expand Down Expand Up @@ -139,7 +139,7 @@ def _calculate_new_state(
an AssertionError. Non-boundary blocks never calculate their own state, they get it from their parent block
instead.
"""
height = boundary_block.get_height()
height = boundary_block.static_metadata.height
criteria = self._feature_settings.features.get(feature)
evaluation_interval = self._feature_settings.evaluation_interval

Expand All @@ -162,7 +162,7 @@ def _calculate_new_state(
# Get the count for this block's parent. Since this is a boundary block, its parent count represents the
# previous evaluation interval count.
parent_block = boundary_block.get_block_parent()
counts = parent_block.get_feature_activation_bit_counts()
counts = parent_block.static_metadata.feature_activation_bit_counts
count = counts[criteria.bit]
threshold = criteria.get_threshold(self._feature_settings)

Expand Down Expand Up @@ -209,8 +209,9 @@ def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'B
Given a block, return its ancestor at a specific height.
Uses the height index if the block is in the best blockchain, or search iteratively otherwise.
"""
assert ancestor_height < block.get_height(), (
f"ancestor height must be lower than the block's height: {ancestor_height} >= {block.get_height()}"
assert ancestor_height < block.static_metadata.height, (
f"ancestor height must be lower than the block's height: "
f"{ancestor_height} >= {block.static_metadata.height}"
)

# It's possible that this method is called before the consensus runs for this block, therefore we do not know
Expand All @@ -219,10 +220,10 @@ def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'B
parent_metadata = parent_block.get_metadata()
assert parent_metadata.validation.is_fully_connected(), 'The parent should always be fully validated.'

if parent_block.get_height() == ancestor_height:
if parent_block.static_metadata.height == ancestor_height:
return parent_block

if not parent_metadata.voided_by and (ancestor := self._tx_storage.get_transaction_by_height(ancestor_height)):
if not parent_metadata.voided_by and (ancestor := self._tx_storage.get_block_by_height(ancestor_height)):
from hathor.transaction import Block
assert isinstance(ancestor, Block)
return ancestor
Expand All @@ -237,11 +238,11 @@ def _get_ancestor_iteratively(self, *, block: 'Block', ancestor_height: int) ->
# TODO: there are further optimizations to be done here, the latest common block height could be persisted in
# metadata, so we could still use the height index if the requested height is before that height.
assert ancestor_height >= 0
assert block.get_height() - ancestor_height <= self._feature_settings.evaluation_interval, (
assert block.static_metadata.height - ancestor_height <= self._feature_settings.evaluation_interval, (
'requested ancestor is deeper than the maximum allowed'
)
ancestor = block
while ancestor.get_height() > ancestor_height:
while ancestor.static_metadata.height > ancestor_height:
ancestor = ancestor.get_block_parent()

return ancestor
2 changes: 1 addition & 1 deletion hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def get_block_features(self, request: Request) -> bytes:

def get_features(self) -> bytes:
best_block = self.tx_storage.get_best_block()
bit_counts = best_block.get_feature_activation_bit_counts()
bit_counts = best_block.static_metadata.feature_activation_bit_counts
features = []

for feature, criteria in self._feature_settings.features.items():
Expand Down
8 changes: 3 additions & 5 deletions hathor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ def _initialize_components_full_verification(self) -> None:
dt = LogDuration(t2 - t1)
dcnt = cnt - cnt2
tx_rate = '?' if dt == 0 else dcnt / dt
h = max(h, tx_meta.height or 0)
h = max(h, (tx_meta.height if isinstance(tx, Block) else 0))
if dt > 30:
ts_date = datetime.datetime.fromtimestamp(self.tx_storage.latest_timestamp)
if h == 0:
Expand All @@ -455,12 +455,10 @@ def _initialize_components_full_verification(self) -> None:

try:
# TODO: deal with invalid tx
tx.calculate_height()
tx._update_parents_children_metadata()

if tx.can_validate_full():
tx.update_initial_metadata()
tx.calculate_min_height()
if tx.is_genesis:
assert tx.validate_checkpoint(self.checkpoints)
assert self.verification_service.validate_full(
Expand Down Expand Up @@ -560,8 +558,6 @@ def _initialize_components_new(self) -> None:
self.tx_storage.pre_init()
assert self.tx_storage.indexes is not None

self._bit_signaling_service.start()

started_at = int(time.time())
last_started_at = self.tx_storage.get_last_started_at()
if last_started_at >= started_at:
Expand All @@ -577,6 +573,8 @@ def _initialize_components_new(self) -> None:
# complex code
self.tx_storage.indexes._manually_initialize(self.tx_storage)

self._bit_signaling_service.start()

# Verify if all checkpoints that exist in the database are correct
try:
self._verify_checkpoints()
Expand Down
2 changes: 1 addition & 1 deletion hathor/mining/block_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def generate_mining_block(self, rng: Random, merge_mined: bool = False, address:
block = cls(outputs=tx_outputs, parents=parents, timestamp=block_timestamp,
data=data or b'', storage=storage, weight=self.weight, signal_bits=self.signal_bits)
if include_metadata:
block._metadata = TransactionMetadata(height=self.height, score=self.score)
block._metadata = TransactionMetadata(score=self.score)
block.get_metadata(use_storage=False)
return block

Expand Down
3 changes: 2 additions & 1 deletion hathor/p2p/resources/mining_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ def render_GET(self, request):
self._settings.P2PKH_VERSION_BYTE.hex() + 'acbfb94571417423c1ed66f706730c4aea516ac5762cccb8'
)
block = self.manager.generate_mining_block(address=burn_address)
block.init_static_metadata_from_storage(self.manager.tx_storage)

height = block.calculate_height() - 1
height = block.static_metadata.height - 1
difficulty = max(int(Weight(block.weight).to_pdiff()), 1)

parent = block.get_block_parent()
Expand Down
1 change: 1 addition & 0 deletions hathor/stratum/stratum.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ def handle_submit(self, params: dict, msgid: Optional[str]) -> None:
'job_id': job_id.hex
})

job.tx.init_static_metadata_from_storage(self.manager.tx_storage)
tx = job.tx.clone()
block_base = tx.get_header_without_nonce()
block_base_hash = sha256d_hash(block_base)
Expand Down
81 changes: 29 additions & 52 deletions hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
from itertools import chain
from math import inf, isfinite, log
from struct import error as StructError, pack
from typing import TYPE_CHECKING, Any, ClassVar, Iterator, Optional
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Iterator, Optional, TypeAlias, TypeVar

from structlog import get_logger

from hathor.checkpoint import Checkpoint
from hathor.conf.get_settings import get_global_settings
from hathor.transaction.exceptions import InvalidOutputValue, WeightError
from hathor.transaction.static_metadata import VertexStaticMetadata
from hathor.transaction.transaction_metadata import TransactionMetadata
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
from hathor.transaction.validation_state import ValidationState
Expand Down Expand Up @@ -117,8 +118,10 @@ def get_cls(self) -> type['BaseTransaction']:

_base_transaction_log = logger.new()

StaticMetadataT = TypeVar('StaticMetadataT', bound=VertexStaticMetadata, covariant=True)

class BaseTransaction(ABC):

class GenericVertex(ABC, Generic[StaticMetadataT]):
"""Hathor base transaction"""

# Even though nonce is serialized with different sizes for tx and blocks
Expand All @@ -128,6 +131,7 @@ class BaseTransaction(ABC):
HEX_BASE = 16

_metadata: Optional[TransactionMetadata]
_static_metadata: StaticMetadataT | None

# Bits extracted from the first byte of the version field. They carry extra information that may be interpreted
# differently by each subclass of BaseTransaction.
Expand Down Expand Up @@ -169,6 +173,7 @@ def __init__(self,
self.parents = parents or []
self.storage = storage
self._hash: VertexId | None = hash # Stored as bytes.
self._static_metadata = None

@classproperty
def log(cls):
Expand Down Expand Up @@ -251,7 +256,7 @@ def __eq__(self, other: object) -> bool:

:raises NotImplement: when one of the transactions do not have a calculated hash
"""
if not isinstance(other, BaseTransaction):
if not isinstance(other, GenericVertex):
return NotImplemented
if self._hash and other._hash:
return self.hash == other.hash
Expand All @@ -267,14 +272,6 @@ def __bytes__(self) -> bytes:
def __hash__(self) -> int:
return hash(self.hash)

@abstractmethod
def calculate_height(self) -> int:
raise NotImplementedError

@abstractmethod
def calculate_min_height(self) -> int:
raise NotImplementedError

@property
def hash(self) -> VertexId:
assert self._hash is not None, 'Vertex hash must be initialized.'
Expand Down Expand Up @@ -619,19 +616,11 @@ def get_metadata(self, *, force_reload: bool = False, use_storage: bool = True)
metadata = self.storage.get_metadata(self.hash)
self._metadata = metadata
if not metadata:
# FIXME: there is code that set use_storage=False but relies on correct height being calculated
# which requires the use of a storage, this is a workaround that should be fixed, places where this
# happens include generating new mining blocks and some tests
height = self.calculate_height() if self.storage else None
score = self.weight if self.is_genesis else 0
min_height = 0 if self.is_genesis else None

metadata = TransactionMetadata(
hash=self._hash,
accumulated_weight=self.weight,
height=height,
score=score,
min_height=min_height
)
self._metadata = metadata
if not metadata.hash:
Expand All @@ -657,8 +646,6 @@ def reset_metadata(self) -> None:
self._metadata.voided_by = {self._settings.PARTIALLY_VALIDATED_ID}
self._metadata._tx_ref = weakref.ref(self)

self._update_height_metadata()

self.storage.save_transaction(self, only_metadata=True)

def update_accumulated_weight(self, *, stop_value: float = inf, save_file: bool = True) -> TransactionMetadata:
Expand Down Expand Up @@ -712,28 +699,11 @@ def update_initial_metadata(self, *, save: bool = True) -> None:

It is called when a new transaction/block is received by HathorManager.
"""
self._update_height_metadata()
self._update_parents_children_metadata()
self.update_reward_lock_metadata()
self._update_feature_activation_bit_counts()
self._update_initial_accumulated_weight()
if save:
assert self.storage is not None
self.storage.save_transaction(self, only_metadata=True)

def _update_height_metadata(self) -> None:
"""Update the vertice height metadata."""
meta = self.get_metadata()
meta.height = self.calculate_height()

def update_reward_lock_metadata(self) -> None:
"""Update the txs/block min_height metadata."""
metadata = self.get_metadata()
min_height = self.calculate_min_height()
if metadata.min_height is not None:
assert metadata.min_height == min_height
metadata.min_height = min_height

def _update_parents_children_metadata(self) -> None:
"""Update the txs/block parent's children metadata."""
assert self._hash is not None
Expand All @@ -745,20 +715,6 @@ def _update_parents_children_metadata(self) -> None:
metadata.children.append(self.hash)
self.storage.save_transaction(parent, only_metadata=True)

def _update_feature_activation_bit_counts(self) -> None:
"""Update the block's feature_activation_bit_counts."""
if not self.is_block:
return
from hathor.transaction import Block
assert isinstance(self, Block)
# This method lazily calculates and stores the value in metadata
self.get_feature_activation_bit_counts()

def _update_initial_accumulated_weight(self) -> None:
"""Update the vertex initial accumulated_weight."""
metadata = self.get_metadata()
metadata.accumulated_weight = self.weight

def update_timestamp(self, now: int) -> None:
"""Update this tx's timestamp

Expand Down Expand Up @@ -865,6 +821,8 @@ def clone(self, *, include_metadata: bool = True, include_storage: bool = True)
:return: Transaction or Block copy
"""
new_tx = self.create_from_struct(self.get_struct())
# static_metadata can be safely copied as it is a frozen dataclass
new_tx.set_static_metadata(self._static_metadata)
if hasattr(self, '_metadata') and include_metadata:
assert self._metadata is not None # FIXME: is this actually true or do we have to check if not None
new_tx._metadata = self._metadata.clone()
Expand All @@ -887,6 +845,25 @@ def is_ready_for_validation(self) -> bool:
return False
return True

@property
def static_metadata(self) -> StaticMetadataT:
assert self._static_metadata is not None
return self._static_metadata

@abstractmethod
def init_static_metadata_from_storage(self, storage: 'TransactionStorage') -> None:
raise NotImplementedError

def set_static_metadata(self, static_metadata: StaticMetadataT | None) -> None:
assert not self._static_metadata or self._static_metadata == static_metadata, (
'trying to set static metadata with different values'
)
self._static_metadata = static_metadata


Vertex: TypeAlias = GenericVertex[VertexStaticMetadata]
BaseTransaction: TypeAlias = Vertex


class TxInput:
_tx: BaseTransaction # XXX: used for caching on hathor.transaction.Transaction.get_spent_tx
Expand Down
Loading