diff --git a/hathor/builder/builder.py b/hathor/builder/builder.py index 393634406..f259eb93b 100644 --- a/hathor/builder/builder.py +++ b/hathor/builder/builder.py @@ -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 diff --git a/hathor/builder/cli_builder.py b/hathor/builder/cli_builder.py index 2b26557bd..3601f60f9 100644 --- a/hathor/builder/cli_builder.py +++ b/hathor/builder/cli_builder.py @@ -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() diff --git a/hathor/event/model/event_data.py b/hathor/event/model/event_data.py index cf22fa424..76b9239be 100644 --- a/hathor/event/model/event_data.py +++ b/hathor/event/model/event_data.py @@ -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) diff --git a/hathor/feature_activation/feature_service.py b/hathor/feature_activation/feature_service.py index caadb62fb..bc3003825 100644 --- a/hathor/feature_activation/feature_service.py +++ b/hathor/feature_activation/feature_service.py @@ -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) @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/hathor/feature_activation/resources/feature.py b/hathor/feature_activation/resources/feature.py index f24579ddc..f39fb1a37 100644 --- a/hathor/feature_activation/resources/feature.py +++ b/hathor/feature_activation/resources/feature.py @@ -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(): diff --git a/hathor/manager.py b/hathor/manager.py index 362cbcfcb..22733f8e4 100644 --- a/hathor/manager.py +++ b/hathor/manager.py @@ -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: @@ -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( @@ -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: @@ -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() diff --git a/hathor/mining/block_template.py b/hathor/mining/block_template.py index 54e690d95..5411cf446 100644 --- a/hathor/mining/block_template.py +++ b/hathor/mining/block_template.py @@ -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 diff --git a/hathor/p2p/resources/mining_info.py b/hathor/p2p/resources/mining_info.py index 180e3876e..454264386 100644 --- a/hathor/p2p/resources/mining_info.py +++ b/hathor/p2p/resources/mining_info.py @@ -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() diff --git a/hathor/stratum/stratum.py b/hathor/stratum/stratum.py index 6cc6d7dea..5de65dca1 100644 --- a/hathor/stratum/stratum.py +++ b/hathor/stratum/stratum.py @@ -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) diff --git a/hathor/transaction/base_transaction.py b/hathor/transaction/base_transaction.py index 56898d6f9..0c2b41deb 100644 --- a/hathor/transaction/base_transaction.py +++ b/hathor/transaction/base_transaction.py @@ -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 @@ -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 @@ -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. @@ -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): @@ -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 @@ -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.' @@ -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: @@ -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: @@ -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 @@ -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 @@ -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() @@ -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 diff --git a/hathor/transaction/block.py b/hathor/transaction/block.py index 80f9ee67d..7ef6e001f 100644 --- a/hathor/transaction/block.py +++ b/hathor/transaction/block.py @@ -13,19 +13,20 @@ # limitations under the License. import base64 -from itertools import starmap, zip_longest -from operator import add from struct import pack from typing import TYPE_CHECKING, Any, Optional +from typing_extensions import override + from hathor.checkpoint import Checkpoint from hathor.feature_activation.feature import Feature from hathor.feature_activation.model.feature_state import FeatureState from hathor.profiler import get_cpu_profiler -from hathor.transaction import BaseTransaction, TxOutput, TxVersion +from hathor.transaction import TxOutput, TxVersion +from hathor.transaction.base_transaction import GenericVertex from hathor.transaction.exceptions import CheckpointError +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len -from hathor.util import not_none from hathor.utils.int import get_bit_list if TYPE_CHECKING: @@ -40,7 +41,7 @@ _SIGHASH_ALL_FORMAT_STRING = '!BBBB' -class Block(BaseTransaction): +class Block(GenericVertex[BlockStaticMetadata]): SERIALIZATION_NONCE_SIZE = 16 def __init__(self, @@ -89,64 +90,6 @@ def create_from_struct(cls, struct_bytes: bytes, storage: Optional['TransactionS return blc - def calculate_height(self) -> int: - """Return the height of the block, i.e., the number of blocks since genesis""" - if self.is_genesis: - return 0 - assert self.storage is not None - parent_block = self.get_block_parent() - return parent_block.get_height() + 1 - - def calculate_min_height(self) -> int: - """The minimum height the next block needs to have, basically the maximum min-height of this block's parents. - """ - assert self.storage is not None - # maximum min-height of any parent tx - min_height = 0 - for tx_hash in self.get_tx_parents(): - tx = self.storage.get_transaction(tx_hash) - tx_min_height = tx.get_metadata().min_height - min_height = max(min_height, not_none(tx_min_height)) - - return min_height - - def get_feature_activation_bit_counts(self) -> list[int]: - """ - Lazily calculates the feature_activation_bit_counts metadata attribute, which is a list of feature activation - bit counts. After it's calculated for the first time, it's persisted in block metadata and must not be changed. - - Each list index corresponds to a bit position, and its respective value is the rolling count of active bits - from the previous boundary block up to this block, including it. LSB is on the left. - """ - metadata = self.get_metadata() - - if metadata.feature_activation_bit_counts is not None: - return metadata.feature_activation_bit_counts - - previous_counts = self._get_previous_feature_activation_bit_counts() - bit_list = self._get_feature_activation_bit_list() - - count_and_bit_pairs = zip_longest(previous_counts, bit_list, fillvalue=0) - updated_counts = starmap(add, count_and_bit_pairs) - metadata.feature_activation_bit_counts = list(updated_counts) - - return metadata.feature_activation_bit_counts - - def _get_previous_feature_activation_bit_counts(self) -> list[int]: - """ - Returns the feature_activation_bit_counts metadata attribute from the parent block, - or no previous counts if this is a boundary block. - """ - evaluation_interval = self._settings.FEATURE_ACTIVATION.evaluation_interval - is_boundary_block = self.calculate_height() % evaluation_interval == 0 - - if is_boundary_block: - return [] - - parent_block = self.get_block_parent() - - return parent_block.get_feature_activation_bit_counts() - def get_next_block_best_chain_hash(self) -> Optional[bytes]: """Return the hash of the next block in the best blockchain. The blockchain is written from left-to-righ (->), meaning the next block has a greater height. @@ -340,9 +283,7 @@ def get_base_hash(self) -> bytes: def get_height(self) -> int: """Returns the block's height.""" - meta = self.get_metadata() - assert meta.height is not None - return meta.height + return self.static_metadata.height def _get_feature_activation_bit_list(self) -> list[int]: """ @@ -401,3 +342,8 @@ def get_feature_activation_bit_value(self, bit: int) -> int: bit_list = self._get_feature_activation_bit_list() return bit_list[bit] + + @override + def init_static_metadata_from_storage(self, storage: 'TransactionStorage') -> None: + static_metadata = BlockStaticMetadata.create_from_storage(self, self._settings, storage) + self.set_static_metadata(static_metadata) diff --git a/hathor/transaction/resources/decode_tx.py b/hathor/transaction/resources/decode_tx.py index 64274040c..9c87445b8 100644 --- a/hathor/transaction/resources/decode_tx.py +++ b/hathor/transaction/resources/decode_tx.py @@ -52,6 +52,7 @@ def render_GET(self, request): try: tx_bytes = bytes.fromhex(parsed['args']['hex_tx']) tx = tx_or_block_from_bytes(tx_bytes) + tx.init_static_metadata_from_storage(self.manager.tx_storage) tx.storage = self.manager.tx_storage data = get_tx_extra_data(tx) except ValueError: diff --git a/hathor/transaction/resources/mining.py b/hathor/transaction/resources/mining.py index eaa80abfd..13280717c 100644 --- a/hathor/transaction/resources/mining.py +++ b/hathor/transaction/resources/mining.py @@ -77,6 +77,7 @@ def render_GET(self, request): # get block # XXX: miner can edit block data and output_script, so it's fine if address is None block = self.manager.generate_mining_block(address=address, merge_mined=merged_mining) + block.init_static_metadata_from_storage(self.manager.tx_storage) # serialize data = block.to_json(include_metadata=True) diff --git a/hathor/transaction/static_metadata.py b/hathor/transaction/static_metadata.py new file mode 100644 index 000000000..a7bd39f08 --- /dev/null +++ b/hathor/transaction/static_metadata.py @@ -0,0 +1,230 @@ +# Copyright 2024 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. + +import dataclasses +from abc import ABC +from dataclasses import dataclass +from itertools import chain, starmap, zip_longest +from operator import add +from typing import TYPE_CHECKING, Callable + +from typing_extensions import Self + +from hathor.conf.settings import HathorSettings +from hathor.types import VertexId +from hathor.util import json_dumpb, json_loadb + +if TYPE_CHECKING: + from hathor.transaction import BaseTransaction, Block, Transaction + from hathor.transaction.storage import TransactionStorage + + +@dataclass(slots=True, frozen=True, kw_only=True) +class VertexStaticMetadata(ABC): + # XXX: this is only used to defer the reward-lock verification from the transaction spending a reward to the first + # block that confirming this transaction, it is important to always have this set to be able to distinguish an old + # metadata (that does not have this calculated, from a tx with a new format that does have this calculated) + min_height: int + + def to_bytes(self) -> bytes: + return json_dumpb(dataclasses.asdict(self)) + + @classmethod + def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticMetadata': + from hathor.transaction import Block, Transaction + json_dict = json_loadb(data) + + if isinstance(target, Block): + return BlockStaticMetadata(**json_dict) + + if isinstance(target, Transaction): + return TransactionStaticMetadata(**json_dict) + + raise NotImplementedError + + +@dataclass(slots=True, frozen=True, kw_only=True) +class BlockStaticMetadata(VertexStaticMetadata): + height: int + # A list of feature activation bit counts. Must only be used by Blocks, is None otherwise. + # Each list index corresponds to a bit position, and its respective value is the rolling count of active bits from + # the previous boundary block up to this block, including it. LSB is on the left. + feature_activation_bit_counts: list[int] + + @classmethod + def create_from_storage(cls, block: 'Block', settings: HathorSettings, storage: 'TransactionStorage') -> Self: + return cls.create(block, settings, storage.get_vertex) + + @classmethod + def create( + cls, + block: 'Block', + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'BaseTransaction'] + ) -> Self: + height = cls._calculate_height(block, vertex_getter) + min_height = cls._calculate_min_height(block, vertex_getter) + feature_activation_bit_counts = cls._calculate_feature_activation_bit_counts( + block, + height, + settings, + vertex_getter, + ) + + return cls( + height=height, + min_height=min_height, + feature_activation_bit_counts=feature_activation_bit_counts, + ) + + @staticmethod + def _calculate_height(block: 'Block', vertex_getter: Callable[[VertexId], 'BaseTransaction']) -> int: + """Return the height of the block, i.e., the number of blocks since genesis""" + if block.is_genesis: + return 0 + + from hathor.transaction import Block + parent_hash = block.get_block_parent_hash() + parent_block = vertex_getter(parent_hash) + assert isinstance(parent_block, Block) + return parent_block.static_metadata.height + 1 + + @staticmethod + def _calculate_min_height(block: 'Block', vertex_getter: Callable[[VertexId], 'BaseTransaction']) -> int: + """The minimum height the next block needs to have, basically the maximum min-height of this block's parents. + """ + # maximum min-height of any parent tx + min_height = 0 + for tx_hash in block.get_tx_parents(): + tx = vertex_getter(tx_hash) + min_height = max(min_height, tx.static_metadata.min_height) + + return min_height + + @classmethod + def _calculate_feature_activation_bit_counts( + cls, + block: 'Block', + height: int, + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'BaseTransaction'], + ) -> list[int]: + """ + Lazily calculates the feature_activation_bit_counts metadata attribute, which is a list of feature activation + bit counts. After it's calculated for the first time, it's persisted in block metadata and must not be changed. + + Each list index corresponds to a bit position, and its respective value is the rolling count of active bits + from the previous boundary block up to this block, including it. LSB is on the left. + """ + previous_counts = cls._get_previous_feature_activation_bit_counts(block, height, settings, vertex_getter) + bit_list = block._get_feature_activation_bit_list() + + count_and_bit_pairs = zip_longest(previous_counts, bit_list, fillvalue=0) + updated_counts = starmap(add, count_and_bit_pairs) + return list(updated_counts) + + @staticmethod + def _get_previous_feature_activation_bit_counts( + block: 'Block', + height: int, + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'BaseTransaction'], + ) -> list[int]: + """ + Returns the feature_activation_bit_counts metadata attribute from the parent block, + or no previous counts if this is a boundary block. + """ + evaluation_interval = settings.FEATURE_ACTIVATION.evaluation_interval + is_boundary_block = height % evaluation_interval == 0 + + if is_boundary_block: + return [] + + from hathor.transaction import Block + parent_hash = block.get_block_parent_hash() + parent_block = vertex_getter(parent_hash) + assert isinstance(parent_block, Block) + + return parent_block.static_metadata.feature_activation_bit_counts + + +@dataclass(slots=True, frozen=True, kw_only=True) +class TransactionStaticMetadata(VertexStaticMetadata): + @classmethod + def create_from_storage(cls, tx: 'Transaction', settings: HathorSettings, storage: 'TransactionStorage') -> Self: + return cls.create(tx, settings, storage.get_vertex) + + @classmethod + def create( + cls, + tx: 'Transaction', + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'BaseTransaction'], + ) -> Self: + min_height = cls._calculate_min_height( + tx, + settings, + vertex_getter=vertex_getter, + ) + + return cls( + min_height=min_height + ) + + @classmethod + def _calculate_min_height( + cls, + tx: 'Transaction', + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'BaseTransaction'], + ) -> int: + """Calculates the min height the first block confirming this tx needs to have for reward lock verification.""" + if tx.is_genesis: + return 0 + + return max( + # 1) don't drop the min height of any parent tx or input tx + cls._calculate_inherited_min_height(tx, vertex_getter), + # 2) include the min height for any reward being spent + cls._calculate_my_min_height(tx, settings, vertex_getter), + ) + + @staticmethod + def _calculate_inherited_min_height( + tx: 'Transaction', + vertex_getter: Callable[[VertexId], 'BaseTransaction'] + ) -> int: + """ Calculates min height inherited from any input or parent""" + min_height = 0 + iter_parents = tx.get_tx_parents() + iter_inputs = (tx_input.tx_id for tx_input in tx.inputs) + for vertex_id in chain(iter_parents, iter_inputs): + vertex = vertex_getter(vertex_id) + min_height = max(min_height, vertex.static_metadata.min_height) + return min_height + + @staticmethod + def _calculate_my_min_height( + tx: 'Transaction', + settings: HathorSettings, + vertex_getter: Callable[[VertexId], 'BaseTransaction'], + ) -> int: + """ Calculates min height derived from own spent block rewards""" + from hathor.transaction import Block + min_height = 0 + for tx_input in tx.inputs: + spent_tx = vertex_getter(tx_input.tx_id) + if isinstance(spent_tx, Block): + min_height = max(min_height, spent_tx.static_metadata.height + settings.REWARD_SPEND_MIN_BLOCKS + 1) + return min_height diff --git a/hathor/transaction/storage/cache_storage.py b/hathor/transaction/storage/cache_storage.py index f8f058e5f..08c966387 100644 --- a/hathor/transaction/storage/cache_storage.py +++ b/hathor/transaction/storage/cache_storage.py @@ -16,13 +16,16 @@ from typing import Any, Iterator, Optional from twisted.internet import threads +from typing_extensions import override from hathor.indexes import IndexesManager from hathor.reactor import ReactorProtocol as Reactor from hathor.transaction import BaseTransaction +from hathor.transaction.static_metadata import VertexStaticMetadata from hathor.transaction.storage.migrations import MigrationState from hathor.transaction.storage.transaction_storage import BaseTransactionStorage from hathor.transaction.storage.tx_allow_scope import TxAllowScope +from hathor.types import VertexId class TransactionCacheStorage(BaseTransactionStorage): @@ -154,6 +157,14 @@ def save_transaction(self, tx: 'BaseTransaction', *, only_metadata: bool = False # call super which adds to index if needed super().save_transaction(tx, only_metadata=only_metadata) + @override + def _save_static_metadata(self, tx: BaseTransaction) -> None: + self.store._save_static_metadata(tx) + + @override + def _get_static_metadata(self, vertex: BaseTransaction) -> VertexStaticMetadata | None: + return self.store._get_static_metadata(vertex) + def get_all_genesis(self) -> set[BaseTransaction]: return self.store.get_all_genesis() @@ -235,3 +246,7 @@ def get_value(self, key: str) -> Optional[str]: def flush(self): self._flush_to_storage(self.dirty_txs.copy()) + + @override + def iter_all_raw_metadata(self) -> Iterator[tuple[VertexId, dict[str, Any]]]: + return self.store.iter_all_raw_metadata() diff --git a/hathor/transaction/storage/memory_storage.py b/hathor/transaction/storage/memory_storage.py index 25dba96ee..45146c689 100644 --- a/hathor/transaction/storage/memory_storage.py +++ b/hathor/transaction/storage/memory_storage.py @@ -14,12 +14,16 @@ from typing import Any, Iterator, Optional, TypeVar +from typing_extensions import override + from hathor.indexes import IndexesManager +from hathor.transaction import BaseTransaction +from hathor.transaction.static_metadata import VertexStaticMetadata from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.transaction.storage.migrations import MigrationState from hathor.transaction.storage.transaction_storage import BaseTransactionStorage -from hathor.transaction.transaction import BaseTransaction from hathor.transaction.transaction_metadata import TransactionMetadata +from hathor.types import VertexId _Clonable = TypeVar('_Clonable', BaseTransaction, TransactionMetadata) @@ -33,6 +37,7 @@ def __init__(self, indexes: Optional[IndexesManager] = None, *, _clone_if_needed """ self.transactions: dict[bytes, BaseTransaction] = {} self.metadata: dict[bytes, TransactionMetadata] = {} + self._static_metadata: dict[bytes, VertexStaticMetadata] = {} # Store custom key/value attributes self.attributes: dict[str, Any] = {} self._clone_if_needed = _clone_if_needed @@ -76,6 +81,14 @@ def _save_transaction(self, tx: BaseTransaction, *, only_metadata: bool = False) if meta: self.metadata[tx.hash] = self._clone(meta) + @override + def _save_static_metadata(self, tx: BaseTransaction) -> None: + self._static_metadata[tx.hash] = tx.static_metadata + + @override + def _get_static_metadata(self, vertex: BaseTransaction) -> VertexStaticMetadata | None: + return self._static_metadata.get(vertex.hash) + def transaction_exists(self, hash_bytes: bytes) -> bool: return hash_bytes in self.transactions @@ -110,3 +123,7 @@ def remove_value(self, key: str) -> None: def get_value(self, key: str) -> Optional[str]: return self.attributes.get(key) + + @override + def iter_all_raw_metadata(self) -> Iterator[tuple[VertexId, dict[str, Any]]]: + raise NotImplementedError diff --git a/hathor/transaction/storage/migrations/migrate_static_metadata.py b/hathor/transaction/storage/migrations/migrate_static_metadata.py new file mode 100644 index 000000000..b84ff8a31 --- /dev/null +++ b/hathor/transaction/storage/migrations/migrate_static_metadata.py @@ -0,0 +1,74 @@ +# 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 typing import TYPE_CHECKING +from unittest.mock import Mock + +from structlog import get_logger + +from hathor.conf.get_settings import get_global_settings +from hathor.transaction import BaseTransaction +from hathor.transaction.static_metadata import BlockStaticMetadata, TransactionStaticMetadata, VertexStaticMetadata +from hathor.transaction.storage.migrations import BaseMigration +from hathor.util import progress + +if TYPE_CHECKING: + from hathor.transaction.storage import TransactionStorage + +logger = get_logger() + + +class Migration(BaseMigration): + def skip_empty_db(self) -> bool: + return True + + def get_db_name(self) -> str: + return 'migrate_static_metadata' + + def run(self, storage: 'TransactionStorage') -> None: + log = logger.new() + settings = get_global_settings() + metadata_iter = storage.iter_all_raw_metadata() + + for vertex_id, raw_metadata in progress(metadata_iter, log=log, total=None): + height = raw_metadata['height'] + min_height = raw_metadata['min_height'] + bit_counts = raw_metadata.get('feature_activation_bit_counts') + + assert isinstance(height, int) + assert isinstance(min_height, int) + + static_metadata: VertexStaticMetadata + is_block = (vertex_id == settings.GENESIS_BLOCK_HASH or height != 0) + + if is_block: + assert isinstance(bit_counts, list) + for item in bit_counts: + assert isinstance(item, int) + + static_metadata = BlockStaticMetadata( + height=height, + min_height=min_height, + feature_activation_bit_counts=bit_counts + ) + else: + assert bit_counts is None or bit_counts == [] + static_metadata = TransactionStaticMetadata( + min_height=min_height + ) + + vertex = Mock(spec_set=BaseTransaction) + vertex.hash = vertex_id + vertex.static_metadata = static_metadata + storage._save_static_metadata(vertex) diff --git a/hathor/transaction/storage/rocksdb_storage.py b/hathor/transaction/storage/rocksdb_storage.py index ec74e6227..6361448a2 100644 --- a/hathor/transaction/storage/rocksdb_storage.py +++ b/hathor/transaction/storage/rocksdb_storage.py @@ -12,15 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Iterator, Optional +from typing import TYPE_CHECKING, Any, Iterator, Optional from structlog import get_logger +from typing_extensions import override from hathor.indexes import IndexesManager from hathor.storage import RocksDBStorage +from hathor.transaction.static_metadata import VertexStaticMetadata from hathor.transaction.storage.exceptions import TransactionDoesNotExist from hathor.transaction.storage.migrations import MigrationState from hathor.transaction.storage.transaction_storage import BaseTransactionStorage +from hathor.types import VertexId from hathor.util import json_dumpb, json_loadb if TYPE_CHECKING: @@ -33,6 +36,7 @@ _DB_NAME = 'data_v2.db' _CF_NAME_TX = b'tx' _CF_NAME_META = b'meta' +_CF_NAME_STATIC_META = b'static-meta' _CF_NAME_ATTR = b'attr' _CF_NAME_MIGRATIONS = b'migrations' @@ -46,6 +50,7 @@ class TransactionRocksDBStorage(BaseTransactionStorage): def __init__(self, rocksdb_storage: RocksDBStorage, indexes: Optional[IndexesManager] = None): self._cf_tx = rocksdb_storage.get_or_create_column_family(_CF_NAME_TX) self._cf_meta = rocksdb_storage.get_or_create_column_family(_CF_NAME_META) + self._cf_static_meta = rocksdb_storage.get_or_create_column_family(_CF_NAME_STATIC_META) self._cf_attr = rocksdb_storage.get_or_create_column_family(_CF_NAME_ATTR) self._cf_migrations = rocksdb_storage.get_or_create_column_family(_CF_NAME_MIGRATIONS) @@ -84,6 +89,7 @@ def remove_transaction(self, tx: 'BaseTransaction') -> None: super().remove_transaction(tx) self._db.delete((self._cf_tx, tx.hash)) self._db.delete((self._cf_meta, tx.hash)) + self._db.delete((self._cf_static_meta, tx.hash)) self._remove_from_weakref(tx) def save_transaction(self, tx: 'BaseTransaction', *, only_metadata: bool = False) -> None: @@ -99,6 +105,15 @@ def _save_transaction(self, tx: 'BaseTransaction', *, only_metadata: bool = Fals meta_data = self._meta_to_bytes(tx.get_metadata(use_storage=False)) self._db.put((self._cf_meta, key), meta_data) + @override + def _save_static_metadata(self, tx: 'BaseTransaction') -> None: + self._db.put((self._cf_static_meta, tx.hash), tx.static_metadata.to_bytes()) + + @override + def _get_static_metadata(self, vertex: 'BaseTransaction') -> VertexStaticMetadata | None: + data = self._db.get((self._cf_static_meta, vertex.hash)) + return VertexStaticMetadata.from_bytes(data, target=vertex) if data else None + def transaction_exists(self, hash_bytes: bytes) -> bool: may_exist, _ = self._db.key_may_exist((self._cf_tx, hash_bytes)) if not may_exist: @@ -204,3 +219,11 @@ def get_value(self, key: str) -> Optional[str]: return None else: return data.decode() + + @override + def iter_all_raw_metadata(self) -> Iterator[tuple[VertexId, dict[str, Any]]]: + items = self._db.iteritems(self._cf_meta) + items.seek_to_first() + + for (_, vertex_id), metadata_bytes in items: + yield vertex_id, json_loadb(metadata_bytes) diff --git a/hathor/transaction/storage/transaction_storage.py b/hathor/transaction/storage/transaction_storage.py index 9b90af63f..7a8dec085 100644 --- a/hathor/transaction/storage/transaction_storage.py +++ b/hathor/transaction/storage/transaction_storage.py @@ -32,6 +32,7 @@ from hathor.transaction.base_transaction import BaseTransaction, TxOutput from hathor.transaction.block import Block from hathor.transaction.exceptions import RewardLocked +from hathor.transaction.static_metadata import VertexStaticMetadata from hathor.transaction.storage.exceptions import ( TransactionDoesNotExist, TransactionIsNotABlock, @@ -43,6 +44,7 @@ add_feature_activation_bit_counts_metadata, add_feature_activation_bit_counts_metadata2, add_min_height_metadata, + migrate_static_metadata, remove_first_nop_features, remove_second_nop_features, ) @@ -100,6 +102,7 @@ class TransactionStorage(ABC): remove_first_nop_features.Migration, add_feature_activation_bit_counts_metadata2.Migration, remove_second_nop_features.Migration, + migrate_static_metadata.Migration, ] _migrations: list[BaseMigration] @@ -331,6 +334,7 @@ def _save_or_verify_genesis(self) -> None: ] for tx in genesis_txs: + tx.init_static_metadata_from_storage(self) try: tx2 = self.get_transaction(tx.hash) assert tx == tx2 @@ -424,6 +428,11 @@ def save_transaction(self: 'TransactionStorage', tx: BaseTransaction, *, only_me """ meta = tx.get_metadata() self.pre_save_validation(tx, meta) + self._save_static_metadata(tx) + + @abstractmethod + def _save_static_metadata(self, vertex: BaseTransaction) -> None: + raise NotImplementedError def pre_save_validation(self, tx: BaseTransaction, tx_meta: TransactionMetadata) -> None: """ Must be run before every save, will raise AssertionError or TransactionNotInAllowedScopeError @@ -438,7 +447,6 @@ def pre_save_validation(self, tx: BaseTransaction, tx_meta: TransactionMetadata) assert tx.hash == tx_meta.hash, f'{tx.hash.hex()} != {tx_meta.hash.hex()}' self._validate_partial_marker_consistency(tx_meta) self._validate_transaction_in_scope(tx) - self._validate_block_height_metadata(tx) def post_get_validation(self, tx: BaseTransaction) -> None: """ Must be run before every save, will raise AssertionError or TransactionNotInAllowedScopeError @@ -449,7 +457,6 @@ def post_get_validation(self, tx: BaseTransaction) -> None: tx_meta = tx.get_metadata() self._validate_partial_marker_consistency(tx_meta) self._validate_transaction_in_scope(tx) - self._validate_block_height_metadata(tx) def _validate_partial_marker_consistency(self, tx_meta: TransactionMetadata) -> None: voided_by = tx_meta.get_frozen_voided_by() @@ -464,11 +471,6 @@ def _validate_transaction_in_scope(self, tx: BaseTransaction) -> None: tx_meta = tx.get_metadata() raise TransactionNotInAllowedScopeError(tx.hash_hex, self.get_allow_scope().name, tx_meta.validation.name) - def _validate_block_height_metadata(self, tx: BaseTransaction) -> None: - if tx.is_block: - tx_meta = tx.get_metadata() - assert tx_meta.height is not None - @abstractmethod def remove_transaction(self, tx: BaseTransaction) -> None: """Remove the tx. @@ -584,14 +586,20 @@ def get_transaction(self, hash_bytes: bytes) -> BaseTransaction: else: tx = self._get_transaction(hash_bytes) self.post_get_validation(tx) + if tx.is_genesis: + tx.init_static_metadata_from_storage(self) + else: + static_metadata = self._get_static_metadata(tx) + assert static_metadata is not None + tx.set_static_metadata(static_metadata) return tx - def get_transaction_by_height(self, height: int) -> Optional[BaseTransaction]: + def get_block_by_height(self, height: int) -> Optional[Block]: """Returns a transaction from the height index. This is fast.""" assert self.indexes is not None ancestor_hash = self.indexes.height.get(height) - return None if ancestor_hash is None else self.get_transaction(ancestor_hash) + return None if ancestor_hash is None else self.get_block(ancestor_hash) def get_metadata(self, hash_bytes: bytes) -> Optional[TransactionMetadata]: """Returns the transaction metadata with hash `hash_bytes`. @@ -605,6 +613,10 @@ def get_metadata(self, hash_bytes: bytes) -> Optional[TransactionMetadata]: except TransactionDoesNotExist: return None + @abstractmethod + def _get_static_metadata(self, vertex: BaseTransaction) -> VertexStaticMetadata | None: + raise NotImplementedError + def get_all_transactions(self) -> Iterator[BaseTransaction]: """Return all vertices (transactions and blocks) within the allowed scope. """ @@ -614,6 +626,9 @@ def get_all_transactions(self) -> Iterator[BaseTransaction]: scope = self.get_allow_scope() for tx in self._get_all_transactions(): if scope.is_allowed(tx): + static_metadata = self._get_static_metadata(tx) + assert static_metadata is not None + tx.set_static_metadata(static_metadata) yield tx @abstractmethod @@ -1161,6 +1176,10 @@ def get_block(self, block_id: VertexId) -> Block: assert isinstance(block, Block) return block + @abstractmethod + def iter_all_raw_metadata(self) -> Iterator[tuple[VertexId, dict[str, Any]]]: + raise NotImplementedError + class BaseTransactionStorage(TransactionStorage): indexes: Optional[IndexesManager] diff --git a/hathor/transaction/transaction.py b/hathor/transaction/transaction.py index a9d9fec5a..7684d4864 100644 --- a/hathor/transaction/transaction.py +++ b/hathor/transaction/transaction.py @@ -13,20 +13,20 @@ # limitations under the License. import hashlib -from itertools import chain from struct import pack from typing import TYPE_CHECKING, Any, NamedTuple, Optional +from typing_extensions import override + from hathor.checkpoint import Checkpoint from hathor.exception import InvalidNewTransaction from hathor.profiler import get_cpu_profiler -from hathor.reward_lock import iter_spent_rewards -from hathor.transaction import BaseTransaction, TxInput, TxOutput, TxVersion -from hathor.transaction.base_transaction import TX_HASH_SIZE +from hathor.transaction import TxInput, TxOutput, TxVersion +from hathor.transaction.base_transaction import TX_HASH_SIZE, GenericVertex from hathor.transaction.exceptions import InvalidToken +from hathor.transaction.static_metadata import TransactionStaticMetadata from hathor.transaction.util import VerboseCallback, unpack, unpack_len from hathor.types import TokenUid, VertexId -from hathor.util import not_none if TYPE_CHECKING: from hathor.transaction.storage import TransactionStorage # noqa: F401 @@ -51,7 +51,7 @@ class RewardLockedInfo(NamedTuple): blocks_needed: int -class Transaction(BaseTransaction): +class Transaction(GenericVertex[TransactionStaticMetadata]): SERIALIZATION_NONCE_SIZE = 4 @@ -105,41 +105,6 @@ def create_from_struct(cls, struct_bytes: bytes, storage: Optional['TransactionS return tx - def calculate_height(self) -> int: - # XXX: transactions don't have height, using 0 as a placeholder - return 0 - - def calculate_min_height(self) -> int: - """Calculates the min height the first block confirming this tx needs to have for reward lock verification. - - Assumes tx has been fully verified (parents and inputs exist and have complete metadata). - """ - if self.is_genesis: - return 0 - return max( - # 1) don't drop the min height of any parent tx or input tx - self._calculate_inherited_min_height(), - # 2) include the min height for any reward being spent - self._calculate_my_min_height(), - ) - - def _calculate_inherited_min_height(self) -> int: - """ Calculates min height inherited from any input or parent""" - assert self.storage is not None - min_height = 0 - iter_parents = map(self.storage.get_transaction, self.get_tx_parents()) - iter_inputs = map(self.get_spent_tx, self.inputs) - for tx in chain(iter_parents, iter_inputs): - min_height = max(min_height, not_none(tx.get_metadata().min_height)) - return min_height - - def _calculate_my_min_height(self) -> int: - """ Calculates min height derived from own spent block rewards""" - min_height = 0 - for blk in iter_spent_rewards(self, not_none(self.storage)): - min_height = max(min_height, blk.get_height() + self._settings.REWARD_SPEND_MIN_BLOCKS + 1) - return min_height - def get_funds_fields_from_struct(self, buf: bytes, *, verbose: VerboseCallback = None) -> bytes: """ Gets all funds fields for a transaction from a buffer. @@ -375,3 +340,8 @@ def is_spending_voided_tx(self) -> bool: if meta.voided_by: return True return False + + @override + def init_static_metadata_from_storage(self, storage: 'TransactionStorage') -> None: + static_metadata = TransactionStaticMetadata.create_from_storage(self, self._settings, storage) + self.set_static_metadata(static_metadata) diff --git a/hathor/transaction/transaction_metadata.py b/hathor/transaction/transaction_metadata.py index 17ed326a1..ba3a9e6bb 100644 --- a/hathor/transaction/transaction_metadata.py +++ b/hathor/transaction/transaction_metadata.py @@ -18,6 +18,7 @@ from hathor.conf.get_settings import get_global_settings from hathor.feature_activation.feature import Feature from hathor.feature_activation.model.feature_state import FeatureState +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.validation_state import ValidationState from hathor.util import practically_equal @@ -40,17 +41,7 @@ class TransactionMetadata: accumulated_weight: float score: float first_block: Optional[bytes] - height: Optional[int] validation: ValidationState - # XXX: this is only used to defer the reward-lock verification from the transaction spending a reward to the first - # block that confirming this transaction, it is important to always have this set to be able to distinguish an old - # metadata (that does not have this calculated, from a tx with a new format that does have this calculated) - min_height: Optional[int] - - # A list of feature activation bit counts. Must only be used by Blocks, is None otherwise. - # Each list index corresponds to a bit position, and its respective value is the rolling count of active bits from - # the previous boundary block up to this block, including it. LSB is on the left. - feature_activation_bit_counts: Optional[list[int]] # A dict of features in the feature activation process and their respective state. Must only be used by Blocks, # is None otherwise. This is only used for caching, so it can be safely cleared up, as it would be recalculated @@ -69,9 +60,6 @@ def __init__( hash: Optional[bytes] = None, accumulated_weight: float = 0, score: float = 0, - height: Optional[int] = None, - min_height: Optional[int] = None, - feature_activation_bit_counts: Optional[list[int]] = None ) -> None: from hathor.transaction.genesis import is_genesis @@ -118,17 +106,9 @@ def __init__( # If two blocks verify the same parent block and have the same score, both are valid. self.first_block = None - # Height - self.height = height - - # Min height - self.min_height = min_height - # Validation self.validation = ValidationState.INITIAL - self.feature_activation_bit_counts = feature_activation_bit_counts - settings = get_global_settings() # Genesis specific: @@ -192,7 +172,7 @@ def __eq__(self, other: Any) -> bool: return False for field in ['hash', 'conflict_with', 'voided_by', 'received_by', 'children', 'accumulated_weight', 'twins', 'score', 'first_block', 'validation', - 'min_height', 'feature_activation_bit_counts', 'feature_states']: + 'feature_states']: if (getattr(self, field) or None) != (getattr(other, field) or None): return False @@ -225,9 +205,12 @@ def to_json(self) -> dict[str, Any]: data['twins'] = [x.hex() for x in self.twins] data['accumulated_weight'] = self.accumulated_weight data['score'] = self.score - data['height'] = self.height data['min_height'] = self.min_height - data['feature_activation_bit_counts'] = self.feature_activation_bit_counts + + from hathor.transaction import Block + if isinstance(self.get_tx(), Block): + data['height'] = self.height + data['feature_activation_bit_counts'] = self.feature_activation_bit_counts if self.feature_states is not None: data['feature_states'] = {feature.value: state.value for feature, state in self.feature_states.items()} @@ -277,9 +260,6 @@ def create_from_json(cls, data: dict[str, Any]) -> 'TransactionMetadata': meta.accumulated_weight = data['accumulated_weight'] meta.score = data.get('score', 0) - meta.height = data.get('height', 0) # XXX: should we calculate the height if it's not defined? - meta.min_height = data.get('min_height') - meta.feature_activation_bit_counts = data.get('feature_activation_bit_counts', []) feature_states_raw = data.get('feature_states') if feature_states_raw: @@ -331,3 +311,19 @@ def is_in_voided_by(self, item: bytes) -> bool: if self.voided_by is None: return False return item in self.voided_by + + @property + def height(self) -> int: + static_metadata = self.get_tx().static_metadata + assert isinstance(static_metadata, BlockStaticMetadata) + return static_metadata.height + + @property + def min_height(self) -> int: + return self.get_tx().static_metadata.min_height + + @property + def feature_activation_bit_counts(self) -> list[int]: + static_metadata = self.get_tx().static_metadata + assert isinstance(static_metadata, BlockStaticMetadata) + return static_metadata.feature_activation_bit_counts diff --git a/hathor/util.py b/hathor/util.py index cd1f0b090..d4eccd3da 100644 --- a/hathor/util.py +++ b/hathor/util.py @@ -478,8 +478,9 @@ def _tx_progress(iter_tx: Iterator['BaseTransaction'], *, log: 'structlog.stdlib log.warn('iterator was slow to yield', took_sec=dt_next) # XXX: this is only informative and made to work with either partially/fully validated blocks/transactions - meta = tx.get_metadata() - if meta.height: + from hathor.transaction import Block + if isinstance(tx, Block): + meta = tx.get_metadata() h = max(h, meta.height) ts_tx = max(ts_tx, tx.timestamp) diff --git a/hathor/verification/verification_service.py b/hathor/verification/verification_service.py index efa18c6f6..04d77ec05 100644 --- a/hathor/verification/verification_service.py +++ b/hathor/verification/verification_service.py @@ -16,6 +16,7 @@ from hathor.profiler import get_cpu_profiler from hathor.transaction import BaseTransaction, Block, MergeMinedBlock, Transaction, TxVersion +from hathor.transaction.storage import TransactionStorage from hathor.transaction.token_creation_tx import TokenCreationTransaction from hathor.transaction.transaction import TokenInfo from hathor.transaction.validation_state import ValidationState @@ -26,10 +27,11 @@ class VerificationService: - __slots__ = ('verifiers', ) + __slots__ = ('verifiers', '_tx_storage') - def __init__(self, *, verifiers: VertexVerifiers) -> None: + def __init__(self, *, verifiers: VertexVerifiers, tx_storage: TransactionStorage | None = None) -> None: self.verifiers = verifiers + self._tx_storage = tx_storage def validate_basic(self, vertex: BaseTransaction, *, skip_block_weight_verification: bool = False) -> bool: """ Run basic validations (all that are possible without dependencies) and update the validation state. @@ -57,9 +59,11 @@ def validate_full( If no exception is raised, the ValidationState will end up as `FULL` or `CHECKPOINT_FULL` and return `True`. """ + assert self._tx_storage is not None from hathor.transaction.transaction_metadata import ValidationState meta = vertex.get_metadata() + vertex.init_static_metadata_from_storage(self._tx_storage) # skip full validation when it is a checkpoint if meta.validation.is_checkpoint(): diff --git a/hathor/vertex_handler/vertex_handler.py b/hathor/vertex_handler/vertex_handler.py index 01b2372fa..862677f84 100644 --- a/hathor/vertex_handler/vertex_handler.py +++ b/hathor/vertex_handler/vertex_handler.py @@ -150,8 +150,6 @@ def _validate_vertex( return False if not metadata.validation.is_fully_connected(): - # TODO: Remove this from here after a refactor in metadata initialization - vertex.update_reward_lock_metadata() try: self._verification_service.validate_full(vertex, reject_locked_reward=reject_locked_reward) except HathorError as e: diff --git a/hathor/wallet/resources/send_tokens.py b/hathor/wallet/resources/send_tokens.py index 936faa2e9..89fc04f08 100644 --- a/hathor/wallet/resources/send_tokens.py +++ b/hathor/wallet/resources/send_tokens.py @@ -127,7 +127,7 @@ def _render_POST_thread(self, values: dict[str, Any], request: Request) -> Union weight = self.manager.daa.minimum_tx_weight(tx) tx.weight = weight self.manager.cpu_mining_service.resolve(tx) - tx.update_reward_lock_metadata() + tx.init_static_metadata_from_storage(self.manager.tx_storage) self.manager.verification_service.verify(tx) return tx diff --git a/hathor/wallet/resources/thin_wallet/send_tokens.py b/hathor/wallet/resources/thin_wallet/send_tokens.py index 2ffb10dfc..a20709f3e 100644 --- a/hathor/wallet/resources/thin_wallet/send_tokens.py +++ b/hathor/wallet/resources/thin_wallet/send_tokens.py @@ -270,7 +270,7 @@ def _should_stop(): if context.should_stop_mining_thread: raise CancelledError() context.tx.update_hash() - context.tx.update_reward_lock_metadata() + context.tx.init_static_metadata_from_storage(self.manager.tx_storage) self.manager.verification_service.verify(context.tx) return context diff --git a/tests/feature_activation/test_feature_service.py b/tests/feature_activation/test_feature_service.py index 60c76d8bc..fddcfd5b4 100644 --- a/tests/feature_activation/test_feature_service.py +++ b/tests/feature_activation/test_feature_service.py @@ -12,12 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import cast from unittest.mock import Mock, patch import pytest -from hathor.conf.get_settings import get_global_settings from hathor.feature_activation.feature import Feature from hathor.feature_activation.feature_service import ( BlockIsMissingSignal, @@ -29,15 +27,17 @@ from hathor.feature_activation.model.feature_description import FeatureDescription from hathor.feature_activation.model.feature_state import FeatureState from hathor.feature_activation.settings import Settings as FeatureSettings -from hathor.transaction import Block, TransactionMetadata -from hathor.transaction.storage import TransactionStorage +from hathor.indexes import MemoryIndexesManager +from hathor.transaction import Block +from hathor.transaction.storage import TransactionMemoryStorage, TransactionStorage from hathor.transaction.validation_state import ValidationState +from hathor.util import not_none -def _get_blocks_and_storage() -> tuple[list[Block], TransactionStorage]: - settings = get_global_settings() - genesis_hash = settings.GENESIS_BLOCK_HASH - blocks: list[Block] = [] +@pytest.fixture +def storage() -> TransactionStorage: + indexes = MemoryIndexesManager() + storage = TransactionMemoryStorage(indexes=indexes) feature_activation_bits = [ 0b0000, # 0: boundary block 0b0010, @@ -72,37 +72,19 @@ def _get_blocks_and_storage() -> tuple[list[Block], TransactionStorage]: 0b0000, # 24: boundary block 0b0000, ] - storage = Mock() for height, bits in enumerate(feature_activation_bits): - block_hash = genesis_hash if height == 0 else int.to_bytes(height, length=1, byteorder='big') - block = Block(hash=block_hash, storage=storage, signal_bits=bits) - blocks.append(block) - parent_hash = blocks[height - 1].hash - assert parent_hash is not None - block.parents = [parent_hash] - block._metadata = TransactionMetadata(height=height) - block._metadata.validation = ValidationState.FULL - - block_by_hash = {block.hash: block for block in blocks} - storage.get_transaction = Mock(side_effect=lambda hash_bytes: block_by_hash[hash_bytes]) - storage.get_transaction_by_height = Mock(side_effect=lambda h: blocks[h]) + if height == 0: + continue + parent = not_none(storage.get_block_by_height(height - 1)) + block = Block(signal_bits=bits, parents=[parent.hash], storage=storage) + block.update_hash() + block.get_metadata().validation = ValidationState.FULL + block.init_static_metadata_from_storage(storage) + storage.save_transaction(block) + indexes.height.add_new(height, block.hash, block.timestamp) - return blocks, storage - - -@pytest.fixture -def block_mocks() -> list[Block]: - blocks, _ = _get_blocks_and_storage() - - return blocks - - -@pytest.fixture -def tx_storage() -> TransactionStorage: - _, tx_storage = _get_blocks_and_storage() - - return tx_storage + return storage @pytest.fixture @@ -114,26 +96,26 @@ def feature_settings() -> FeatureSettings: @pytest.fixture -def service(feature_settings: FeatureSettings, tx_storage: TransactionStorage) -> FeatureService: +def service(feature_settings: FeatureSettings, storage: TransactionStorage) -> FeatureService: service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() return service -def test_get_state_genesis(block_mocks: list[Block], service: FeatureService) -> None: - block = block_mocks[0] +def test_get_state_genesis(storage: TransactionStorage, service: FeatureService) -> None: + block = not_none(storage.get_block_by_height(0)) result = service.get_state(block=block, feature=Mock()) assert result == FeatureState.DEFINED @pytest.mark.parametrize('block_height', [0, 1, 2, 3]) -def test_get_state_first_interval(block_mocks: list[Block], service: FeatureService, block_height: int) -> None: - block = block_mocks[block_height] +def test_get_state_first_interval(storage: TransactionStorage, service: FeatureService, block_height: int) -> None: + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Mock()) assert result == FeatureState.DEFINED @@ -149,8 +131,7 @@ def test_get_state_first_interval(block_mocks: list[Block], service: FeatureServ ] ) def test_get_state_from_defined( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, start_height: int, expected_state: FeatureState @@ -168,10 +149,10 @@ def test_get_state_from_defined( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -181,8 +162,7 @@ def test_get_state_from_defined( @pytest.mark.parametrize('block_height', [12, 13, 14, 15, 16, 17]) @pytest.mark.parametrize('timeout_height', [8, 12]) def test_get_state_from_started_to_failed( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, timeout_height: int, ) -> None: @@ -200,10 +180,10 @@ def test_get_state_from_started_to_failed( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -213,8 +193,7 @@ def test_get_state_from_started_to_failed( @pytest.mark.parametrize('block_height', [8, 9, 10, 11]) @pytest.mark.parametrize('timeout_height', [8, 12]) def test_get_state_from_started_to_must_signal_on_timeout( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, timeout_height: int, ) -> None: @@ -232,10 +211,10 @@ def test_get_state_from_started_to_must_signal_on_timeout( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -246,8 +225,7 @@ def test_get_state_from_started_to_must_signal_on_timeout( @pytest.mark.parametrize('block_height', [8, 9, 10, 11]) @pytest.mark.parametrize('default_threshold', [0, 1, 2, 3]) def test_get_state_from_started_to_locked_in_on_default_threshold( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, default_threshold: int ) -> None: @@ -266,10 +244,10 @@ def test_get_state_from_started_to_locked_in_on_default_threshold( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -279,8 +257,7 @@ def test_get_state_from_started_to_locked_in_on_default_threshold( @pytest.mark.parametrize('block_height', [8, 9, 10, 11]) @pytest.mark.parametrize('custom_threshold', [0, 1, 2, 3]) def test_get_state_from_started_to_locked_in_on_custom_threshold( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, custom_threshold: int ) -> None: @@ -298,10 +275,10 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -318,8 +295,7 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( ] ) def test_get_state_from_started_to_started( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, lock_in_on_timeout: bool, timeout_height: int, @@ -338,10 +314,10 @@ def test_get_state_from_started_to_started( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -350,8 +326,7 @@ def test_get_state_from_started_to_started( @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) def test_get_state_from_must_signal_to_locked_in( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, ) -> None: feature_settings = FeatureSettings.construct( @@ -368,10 +343,10 @@ def test_get_state_from_must_signal_to_locked_in( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -381,8 +356,7 @@ def test_get_state_from_must_signal_to_locked_in( @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) @pytest.mark.parametrize('minimum_activation_height', [0, 4, 8, 12, 16]) def test_get_state_from_locked_in_to_active( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, minimum_activation_height: int, ) -> None: @@ -401,10 +375,10 @@ def test_get_state_from_locked_in_to_active( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -414,8 +388,7 @@ def test_get_state_from_locked_in_to_active( @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) @pytest.mark.parametrize('minimum_activation_height', [17, 20, 100]) def test_get_state_from_locked_in_to_locked_in( - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, minimum_activation_height: int, ) -> None: @@ -434,10 +407,10 @@ def test_get_state_from_locked_in_to_locked_in( ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -445,7 +418,7 @@ def test_get_state_from_locked_in_to_locked_in( @pytest.mark.parametrize('block_height', [20, 21, 22, 23]) -def test_get_state_from_active(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: +def test_get_state_from_active(storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -460,10 +433,10 @@ def test_get_state_from_active(block_mocks: list[Block], tx_storage: Transaction ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) @@ -471,7 +444,7 @@ def test_get_state_from_active(block_mocks: list[Block], tx_storage: Transaction @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) -def test_caching_mechanism(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: +def test_caching_mechanism(storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -484,9 +457,9 @@ def test_caching_mechanism(block_mocks: list[Block], tx_storage: TransactionStor ) } ) - service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service = FeatureService(feature_settings=feature_settings, tx_storage=storage) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) calculate_new_state_mock = Mock(wraps=service._calculate_new_state) with patch.object(FeatureService, '_calculate_new_state', calculate_new_state_mock): @@ -503,7 +476,7 @@ def test_caching_mechanism(block_mocks: list[Block], tx_storage: TransactionStor @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) -def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: +def test_is_feature_active(storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -518,10 +491,10 @@ def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStor ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.is_feature_active(block=block, feature=Feature.NOP_FEATURE_1) @@ -529,7 +502,7 @@ def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStor @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) -def test_get_state_from_failed(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None: +def test_get_state_from_failed(storage: TransactionStorage, block_height: int) -> None: feature_settings = FeatureSettings.construct( evaluation_interval=4, features={ @@ -543,25 +516,24 @@ def test_get_state_from_failed(block_mocks: list[Block], tx_storage: Transaction ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.FAILED -def test_get_state_undefined_feature(block_mocks: list[Block], service: FeatureService) -> None: - block = block_mocks[10] - +def test_get_state_undefined_feature(storage: TransactionStorage, service: FeatureService) -> None: + block = not_none(storage.get_block_by_height(10)) result = service.get_state(block=block, feature=Feature.NOP_FEATURE_1) assert result == FeatureState.DEFINED -def test_get_bits_description(tx_storage: TransactionStorage) -> None: +def test_get_bits_description(storage: TransactionStorage) -> None: criteria_mock_1 = Criteria.construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) criteria_mock_2 = Criteria.construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) feature_settings = FeatureSettings.construct( @@ -572,7 +544,7 @@ def test_get_bits_description(tx_storage: TransactionStorage) -> None: ) service = FeatureService( feature_settings=feature_settings, - tx_storage=tx_storage + tx_storage=storage ) service.bit_signaling_service = Mock() @@ -606,14 +578,13 @@ def get_state(self: FeatureService, *, block: Block, feature: Feature) -> Featur ) def test_get_ancestor_at_height_invalid( feature_settings: FeatureSettings, - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, ancestor_height: int ) -> None: - service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service = FeatureService(feature_settings=feature_settings, tx_storage=storage) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) with pytest.raises(AssertionError) as e: service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) @@ -636,21 +607,23 @@ def test_get_ancestor_at_height_invalid( ) def test_get_ancestor_at_height( feature_settings: FeatureSettings, - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, ancestor_height: int ) -> None: - service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service = FeatureService(feature_settings=feature_settings, tx_storage=storage) service.bit_signaling_service = Mock() - block = block_mocks[block_height] - result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) + block = not_none(storage.get_block_by_height(block_height)) + + get_block_by_height_wrapped = Mock(wraps=storage.get_block_by_height) + with patch.object(storage, 'get_block_by_height', get_block_by_height_wrapped): + result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) - assert result == block_mocks[ancestor_height] - assert result.get_height() == ancestor_height - assert cast(Mock, tx_storage.get_transaction_by_height).call_count == ( - 0 if block_height - ancestor_height <= 1 else 1 - ), 'this should only be called if the ancestor is deeper than one parent away' + assert get_block_by_height_wrapped.call_count == ( + 0 if block_height - ancestor_height <= 1 else 1 + ), 'this should only be called if the ancestor is deeper than one parent away' + assert result == storage.get_block_by_height(ancestor_height) + assert result.get_height() == ancestor_height @pytest.mark.parametrize( @@ -665,21 +638,23 @@ def test_get_ancestor_at_height( ) def test_get_ancestor_at_height_voided( feature_settings: FeatureSettings, - block_mocks: list[Block], - tx_storage: TransactionStorage, + storage: TransactionStorage, block_height: int, ancestor_height: int ) -> None: - service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service = FeatureService(feature_settings=feature_settings, tx_storage=storage) service.bit_signaling_service = Mock() - block = block_mocks[block_height] - parent_block = block_mocks[block_height - 1] + block = not_none(storage.get_block_by_height(block_height)) + parent_block = not_none(storage.get_block_by_height(block_height - 1)) parent_block.get_metadata().voided_by = {b'some'} - result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) - assert result == block_mocks[ancestor_height] - assert result.get_height() == ancestor_height - assert cast(Mock, tx_storage.get_transaction_by_height).call_count == 0 + get_block_by_height_wrapped = Mock(wraps=storage.get_block_by_height) + with patch.object(storage, 'get_block_by_height', get_block_by_height_wrapped): + result = service._get_ancestor_at_height(block=block, ancestor_height=ancestor_height) + + assert get_block_by_height_wrapped.call_count == 0 + assert result == storage.get_block_by_height(ancestor_height) + assert result.get_height() == ancestor_height @pytest.mark.parametrize( @@ -709,8 +684,7 @@ def test_get_ancestor_at_height_voided( ] ) def test_check_must_signal( - tx_storage: TransactionStorage, - block_mocks: list[Block], + storage: TransactionStorage, bit: int, threshold: int, block_height: int, @@ -729,9 +703,9 @@ def test_check_must_signal( ) } ) - service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage) + service = FeatureService(feature_settings=feature_settings, tx_storage=storage) service.bit_signaling_service = Mock() - block = block_mocks[block_height] + block = not_none(storage.get_block_by_height(block_height)) result = service.is_signaling_mandatory_features(block) diff --git a/tests/feature_activation/test_feature_simulation.py b/tests/feature_activation/test_feature_simulation.py index 91b077711..15e8bed66 100644 --- a/tests/feature_activation/test_feature_simulation.py +++ b/tests/feature_activation/test_feature_simulation.py @@ -228,7 +228,7 @@ def test_feature(self) -> None: non_signaling_block = manager.generate_mining_block() manager.cpu_mining_service.resolve(non_signaling_block) non_signaling_block.signal_bits = 0b10 - non_signaling_block.update_reward_lock_metadata() + non_signaling_block.init_static_metadata_from_storage(manager.tx_storage) with pytest.raises(BlockMustSignalError): manager.verification_service.verify(non_signaling_block) diff --git a/tests/others/test_metrics.py b/tests/others/test_metrics.py index f799fc961..9a68bf757 100644 --- a/tests/others/test_metrics.py +++ b/tests/others/test_metrics.py @@ -107,6 +107,7 @@ def _init_manager(): b'meta': 0.0, b'attr': 0.0, b'migrations': 0.0, + b'static-meta': 0.0, b'event': 0.0, b'event-metadata': 0.0, b'feature-activation-metadata': 0.0, @@ -160,6 +161,7 @@ def _init_manager(): b'meta': 0.0, b'attr': 0.0, b'migrations': 0.0, + b'static-meta': 0.0, b'event': 0.0, b'event-metadata': 0.0, b'feature-activation-metadata': 0.0, diff --git a/tests/p2p/test_sync.py b/tests/p2p/test_sync.py index 0b23a23e3..de0139d3f 100644 --- a/tests/p2p/test_sync.py +++ b/tests/p2p/test_sync.py @@ -707,8 +707,8 @@ def test_block_sync_checkpoints(self) -> None: conn.run_one_step(debug=False) self.clock.advance(0.1) - self.assertEqual(self.manager1.tx_storage.get_best_block().get_metadata().height, TOTAL_BLOCKS) - self.assertEqual(manager2.tx_storage.get_best_block().get_metadata().height, TOTAL_BLOCKS) + self.assertEqual(self.manager1.tx_storage.get_best_block().static_metadata.height, TOTAL_BLOCKS) + self.assertEqual(manager2.tx_storage.get_best_block().static_metadata.height, TOTAL_BLOCKS) node_sync1 = conn.proto1.state.sync_agent node_sync2 = conn.proto2.state.sync_agent diff --git a/tests/resources/feature/test_feature.py b/tests/resources/feature/test_feature.py index bc6a9083e..8570df16c 100644 --- a/tests/resources/feature/test_feature.py +++ b/tests/resources/feature/test_feature.py @@ -24,20 +24,24 @@ from hathor.feature_activation.resources.feature import FeatureResource from hathor.feature_activation.settings import Settings as FeatureSettings from hathor.transaction import Block +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.storage import TransactionStorage from tests.resources.base_resource import StubSite @pytest.fixture -def web(): - block_mock = Mock(wraps=Block(), spec_set=Block) - block_mock.get_feature_activation_bit_counts = Mock(return_value=[0, 1, 0, 0]) - block_mock.hash_hex = 'some_hash' - block_mock.get_height = Mock(return_value=123) +def web() -> StubSite: + block = Block(hash=b'some_hash') + static_metadata = BlockStaticMetadata( + height=123, + min_height=0, + feature_activation_bit_counts=[0, 1, 0, 0] + ) + block.set_static_metadata(static_metadata) tx_storage = Mock(spec_set=TransactionStorage) - tx_storage.get_best_block = Mock(return_value=block_mock) - tx_storage.get_transaction = Mock(return_value=block_mock) + tx_storage.get_best_block = Mock(return_value=block) + tx_storage.get_transaction = Mock(return_value=block) def get_state(*, block: Block, feature: Feature) -> FeatureState: return FeatureState.ACTIVE if feature is Feature.NOP_FEATURE_1 else FeatureState.STARTED @@ -81,11 +85,11 @@ def get_state(*, block: Block, feature: Feature) -> FeatureState: return StubSite(feature_resource) -def test_get_features(web): +def test_get_features(web: StubSite) -> None: response = web.get('feature') result = response.result.json_value() expected = dict( - block_hash='some_hash', + block_hash=b'some_hash'.hex(), block_height=123, features=[ dict( @@ -116,7 +120,7 @@ def test_get_features(web): assert result == expected -def test_get_block_features(web): +def test_get_block_features(web: StubSite) -> None: response = web.get('feature', args={b'block': b'1234'}) result = response.result.json_value() expected = dict( diff --git a/tests/resources/transaction/test_mining.py b/tests/resources/transaction/test_mining.py index caae54ee2..e412f9043 100644 --- a/tests/resources/transaction/test_mining.py +++ b/tests/resources/transaction/test_mining.py @@ -38,9 +38,9 @@ def test_get_block_template_with_address(self): 'accumulated_weight': 1.0, 'score': 0, 'height': 1, - 'min_height': None, + 'min_height': 0, 'first_block': None, - 'feature_activation_bit_counts': None + 'feature_activation_bit_counts': [0, 0, 0, 0] }, 'tokens': [], 'data': '', @@ -71,9 +71,9 @@ def test_get_block_template_without_address(self): 'accumulated_weight': 1.0, 'score': 0, 'height': 1, - 'min_height': None, + 'min_height': 0, 'first_block': None, - 'feature_activation_bit_counts': None + 'feature_activation_bit_counts': [0, 0, 0, 0] }, 'tokens': [], 'data': '', diff --git a/tests/resources/transaction/test_pushtx.py b/tests/resources/transaction/test_pushtx.py index 7ed5b3e36..638f63bb9 100644 --- a/tests/resources/transaction/test_pushtx.py +++ b/tests/resources/transaction/test_pushtx.py @@ -47,6 +47,7 @@ def get_tx(self, inputs: Optional[list[WalletInputInfo]] = None, tx.timestamp = max(max_ts_spent_tx + 1, int(self.manager.reactor.seconds())) tx.parents = self.manager.get_new_tx_parents(tx.timestamp) self.manager.cpu_mining_service.resolve(tx) + tx.init_static_metadata_from_storage(self.manager.tx_storage) return tx def push_tx(self, data=None): diff --git a/tests/resources/transaction/test_tx.py b/tests/resources/transaction/test_tx.py index 4d4344c0e..cb30b7cbf 100644 --- a/tests/resources/transaction/test_tx.py +++ b/tests/resources/transaction/test_tx.py @@ -3,6 +3,7 @@ from hathor.simulator.utils import add_new_blocks from hathor.transaction import Transaction from hathor.transaction.resources import TransactionResource +from hathor.transaction.static_metadata import TransactionStaticMetadata from hathor.transaction.token_creation_tx import TokenCreationTransaction from hathor.transaction.validation_state import ValidationState from tests import unittest @@ -31,7 +32,7 @@ def test_get_one(self): dict_test['raw'] = genesis_tx.get_struct().hex() dict_test['nonce'] = str(dict_test['nonce']) if genesis_tx.is_block: - dict_test['height'] = genesis_tx.calculate_height() + dict_test['height'] = genesis_tx.static_metadata.height self.assertEqual(data_success['tx'], dict_test) # Test sending hash that does not exist @@ -87,6 +88,7 @@ def test_get_one_known_tx(self): '0248b9e7d6a626f45dec86975b00f4dd53f84f1f0091125250b044e49023fbbd0f74f6093cdd2226fdff3e09a1000002be') tx = Transaction.create_from_struct(bytes.fromhex(tx_hex), self.manager.tx_storage) tx.get_metadata().validation = ValidationState.FULL + tx.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx) tx_parent1_hex = ('0001010102001c382847d8440d05da95420bee2ebeb32bc437f82a9ae47b0745c8a29a7b0d001c382847d844' @@ -99,6 +101,7 @@ def test_get_one_known_tx(self): '8fb080f53a0c9c57ddb000000120') tx_parent1 = Transaction.create_from_struct(bytes.fromhex(tx_parent1_hex), self.manager.tx_storage) tx_parent1.get_metadata().validation = ValidationState.FULL + tx_parent1.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx_parent1) tx_parent2_hex = ('0001000103001f16fe62e3433bcc74b262c11a1fa94fcb38484f4d8fb080f53a0c9c57ddb001006946304402' @@ -111,6 +114,7 @@ def test_get_one_known_tx(self): 'd57709926b76e64763bf19c3f13eeac30000016d') tx_parent2 = Transaction.create_from_struct(bytes.fromhex(tx_parent2_hex), self.manager.tx_storage) tx_parent2.get_metadata().validation = ValidationState.FULL + tx_parent2.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx_parent2) tx_input_hex = ('0001010203007231eee3cb6160d95172a409d634d0866eafc8775f5729fff6a61e7850aba500b3ab76c5337b55' @@ -126,6 +130,7 @@ def test_get_one_known_tx(self): 'cfaf6e7ceb2ba91c9c84009c8174d4a46ebcc789d1989e3dec5b68cffeef239fd8cf86ef62728e2eacee000001b6') tx_input = Transaction.create_from_struct(bytes.fromhex(tx_input_hex), self.manager.tx_storage) tx_input.get_metadata().validation = ValidationState.FULL + tx_input.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx_input) # XXX: this is completely dependant on MemoryTokensIndex implementation, hence use_memory_storage=True @@ -193,6 +198,7 @@ def test_get_one_known_tx_with_authority(self): '5114256caacfb8f6dd13db33000020393') tx = Transaction.create_from_struct(bytes.fromhex(tx_hex), self.manager.tx_storage) tx.get_metadata().validation = ValidationState.FULL + tx.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx) tx_parent1_hex = ('0001010203000023b318c91dcfd4b967b205dc938f9f5e2fd5114256caacfb8f6dd13db330000023b318c91dcfd' @@ -208,6 +214,7 @@ def test_get_one_known_tx_with_authority(self): 'd13db3300038c3d3b69ce90bb88c0c4d6a87b9f0c349e5b10c9b7ce6714f996e512ac16400021261') tx_parent1 = Transaction.create_from_struct(bytes.fromhex(tx_parent1_hex), self.manager.tx_storage) tx_parent1.get_metadata().validation = ValidationState.FULL + tx_parent1.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx_parent1) tx_parent2_hex = ('000201040000476810205cb3625d62897fcdad620e01d66649869329640f5504d77e960d01006a473045022100c' @@ -222,6 +229,7 @@ def test_get_one_known_tx_with_authority(self): tx_parent2_bytes = bytes.fromhex(tx_parent2_hex) tx_parent2 = TokenCreationTransaction.create_from_struct(tx_parent2_bytes, self.manager.tx_storage) tx_parent2.get_metadata().validation = ValidationState.FULL + tx_parent2.set_static_metadata(TransactionStaticMetadata(min_height=0)) self.manager.tx_storage.save_transaction(tx_parent2) # Both inputs are the same as the last parent, so no need to manually add them @@ -514,6 +522,7 @@ def test_partially_validated_not_found(self): '0248b9e7d6a626f45dec86975b00f4dd53f84f1f0091125250b044e49023fbbd0f74f6093cdd2226fdff3e09a1000002be') tx = Transaction.create_from_struct(bytes.fromhex(tx_hex), self.manager.tx_storage) tx.set_validation(ValidationState.BASIC) + tx.set_static_metadata(TransactionStaticMetadata(min_height=0)) with self.manager.tx_storage.allow_partially_validated_context(): self.manager.tx_storage.save_transaction(tx) diff --git a/tests/tx/test_block.py b/tests/tx/test_block.py index c5f698965..40491422b 100644 --- a/tests/tx/test_block.py +++ b/tests/tx/test_block.py @@ -20,26 +20,28 @@ from hathor.conf.settings import HathorSettings from hathor.feature_activation.feature import Feature from hathor.feature_activation.feature_service import BlockIsMissingSignal, BlockIsSignaling, FeatureService -from hathor.transaction import Block, TransactionMetadata +from hathor.indexes import MemoryIndexesManager +from hathor.transaction import Block from hathor.transaction.exceptions import BlockMustSignalError +from hathor.transaction.static_metadata import BlockStaticMetadata from hathor.transaction.storage import TransactionMemoryStorage, TransactionStorage +from hathor.transaction.validation_state import ValidationState +from hathor.util import not_none from hathor.verification.block_verifier import BlockVerifier def test_calculate_feature_activation_bit_counts_genesis(): settings = get_global_settings() storage = TransactionMemoryStorage() - genesis_block = storage.get_transaction(settings.GENESIS_BLOCK_HASH) - assert isinstance(genesis_block, Block) - result = genesis_block.get_feature_activation_bit_counts() + genesis_block = storage.get_block(settings.GENESIS_BLOCK_HASH) - assert result == [0, 0, 0, 0] + assert genesis_block.static_metadata.feature_activation_bit_counts == [0, 0, 0, 0] @pytest.fixture -def block_mocks() -> list[Block]: - settings = get_global_settings() - blocks: list[Block] = [] +def storage() -> TransactionStorage: + indexes = MemoryIndexesManager() + storage = TransactionMemoryStorage(indexes=indexes) feature_activation_bits = [ 0b0000, # 0: boundary block 0b1010, @@ -55,20 +57,18 @@ def block_mocks() -> list[Block]: 0b0000, ] - for i, bits in enumerate(feature_activation_bits): - genesis_hash = settings.GENESIS_BLOCK_HASH - block_hash = genesis_hash if i == 0 else b'some_hash' - - storage = Mock(spec_set=TransactionStorage) - storage.get_metadata = Mock(return_value=None) - - block = Block(hash=block_hash, storage=storage, signal_bits=bits) - blocks.append(block) + for height, bits in enumerate(feature_activation_bits): + if height == 0: + continue + parent = not_none(storage.get_block_by_height(height - 1)) + block = Block(signal_bits=bits, parents=[parent.hash]) + block.update_hash() + block.get_metadata().validation = ValidationState.FULL + block.init_static_metadata_from_storage(storage) + storage.save_transaction(block) + indexes.height.add_new(height, block.hash, block.timestamp) - get_block_parent_mock = Mock(return_value=blocks[i - 1]) - setattr(block, 'get_block_parent', get_block_parent_mock) - - return blocks + return storage @pytest.mark.parametrize( @@ -87,27 +87,24 @@ def block_mocks() -> list[Block]: ] ) def test_calculate_feature_activation_bit_counts( - block_mocks: list[Block], + storage: TransactionStorage, block_height: int, expected_counts: list[int] ) -> None: - block = block_mocks[block_height] - result = block.get_feature_activation_bit_counts() - - assert result == expected_counts + block = not_none(storage.get_block_by_height(block_height)) + assert block.static_metadata.feature_activation_bit_counts == expected_counts def test_get_height(): - block_hash = b'some_hash' - block_height = 10 - metadata = TransactionMetadata(hash=block_hash, height=block_height) - - storage = Mock(spec_set=TransactionStorage) - storage.get_metadata = Mock(side_effect=lambda _hash: metadata if _hash == block_hash else None) - - block = Block(hash=block_hash, storage=storage) + static_metadata = BlockStaticMetadata( + min_height=0, + height=10, + feature_activation_bit_counts=[] + ) + block = Block() + block.set_static_metadata(static_metadata) - assert block.get_height() == block_height + assert block.get_height() == 10 @pytest.mark.parametrize( diff --git a/tests/tx/test_cache_storage.py b/tests/tx/test_cache_storage.py index d9aac999c..27dcaf059 100644 --- a/tests/tx/test_cache_storage.py +++ b/tests/tx/test_cache_storage.py @@ -36,6 +36,7 @@ def _get_new_tx(self, nonce): from hathor.transaction.validation_state import ValidationState tx = Transaction(nonce=nonce, storage=self.cache_storage) tx.update_hash() + tx.init_static_metadata_from_storage(self.cache_storage) meta = TransactionMetadata(hash=tx.hash) meta.validation = ValidationState.FULL tx._metadata = meta diff --git a/tests/tx/test_reward_lock.py b/tests/tx/test_reward_lock.py index 80f6f6e18..144850c06 100644 --- a/tests/tx/test_reward_lock.py +++ b/tests/tx/test_reward_lock.py @@ -61,6 +61,7 @@ def _spend_reward_tx(self, manager, reward_block): input_.data = P2PKH.create_input_data(public_bytes, signature) self.manager.cpu_mining_service.resolve(tx) tx.update_initial_metadata(save=False) + tx.init_static_metadata_from_storage(self.tx_storage) return tx def test_classic_reward_lock(self): diff --git a/tests/tx/test_tokens.py b/tests/tx/test_tokens.py index 0906477e1..c27a0c31e 100644 --- a/tests/tx/test_tokens.py +++ b/tests/tx/test_tokens.py @@ -112,7 +112,7 @@ def test_token_transfer(self): public_bytes, signature = wallet.get_input_aux_data(data_to_sign, wallet.get_private_key(self.address_b58)) tx2.inputs[0].data = P2PKH.create_input_data(public_bytes, signature) self.manager.cpu_mining_service.resolve(tx2) - tx2.update_reward_lock_metadata() + tx2.init_static_metadata_from_storage(self.manager.tx_storage) self.manager.verification_service.verify(tx2) # missing tokens diff --git a/tests/tx/test_tx.py b/tests/tx/test_tx.py index 349731ffd..7a824961a 100644 --- a/tests/tx/test_tx.py +++ b/tests/tx/test_tx.py @@ -189,6 +189,7 @@ def test_children_update(self): children_len.append(len(metadata.children)) # update metadata + tx.init_static_metadata_from_storage(self.tx_storage) tx.update_initial_metadata() # genesis transactions should have only this tx in their children set @@ -250,6 +251,7 @@ def test_merge_mined_no_magic(self): ) ) + b.init_static_metadata_from_storage(self.tx_storage) with self.assertRaises(AuxPowNoMagicError): self._verifiers.merge_mined_block.verify_aux_pow(b) @@ -323,6 +325,8 @@ def test_merge_mined_multiple_magic(self): assert bytes(b1) != bytes(b2) assert b1.calculate_hash() == b2.calculate_hash() + b1.init_static_metadata_from_storage(self.tx_storage) + b2.init_static_metadata_from_storage(self.tx_storage) self._verifiers.merge_mined_block.verify_aux_pow(b1) # OK with self.assertRaises(AuxPowUnexpectedMagicError): self._verifiers.merge_mined_block.verify_aux_pow(b2) @@ -568,7 +572,7 @@ def test_regular_tx(self): _input.data = P2PKH.create_input_data(public_bytes, signature) self.manager.cpu_mining_service.resolve(tx) - tx.update_reward_lock_metadata() + tx.init_static_metadata_from_storage(self.manager.tx_storage) self.manager.verification_service.verify(tx) def test_tx_weight_too_high(self): @@ -907,11 +911,14 @@ def test_tx_version_and_signal_bits(self): self.assertEqual(str(cm.exception), 'version 0x200 must not be larger than one byte') # test serialization doesn't mess up with version + genesis_block = self.genesis_blocks[0] block = Block( signal_bits=0xF0, version=0x0F, nonce=100, - weight=1) + weight=1, + parents=[genesis_block.hash] + ) block2 = block.clone() self.assertEqual(block.signal_bits, block2.signal_bits) self.assertEqual(block.version, block2.version) diff --git a/tests/tx/test_tx_storage.py b/tests/tx/test_tx_storage.py index de377cb9b..1f5104d9a 100644 --- a/tests/tx/test_tx_storage.py +++ b/tests/tx/test_tx_storage.py @@ -62,7 +62,7 @@ def setUp(self): self.block = Block(timestamp=previous_timestamp + 1, weight=12, outputs=[output], parents=block_parents, nonce=100781, storage=self.tx_storage) self.manager.cpu_mining_service.resolve(self.block) - self.block.update_reward_lock_metadata() + self.block.init_static_metadata_from_storage(self.tx_storage) self.manager.verification_service.verify(self.block) self.block.get_metadata().validation = ValidationState.FULL @@ -81,6 +81,7 @@ def setUp(self): parents=tx_parents, storage=self.tx_storage) self.manager.cpu_mining_service.resolve(self.tx) self.tx.get_metadata().validation = ValidationState.FULL + self.tx.init_static_metadata_from_storage(self.tx_storage) # Disable weakref to test the internal methods. Otherwise, most methods return objects from weakref. self.tx_storage._disable_weakref() @@ -374,6 +375,7 @@ def test_allow_scope_context_stacking(self): def test_save_token_creation_tx(self): tx = create_tokens(self.manager, propagate=False) tx.get_metadata().validation = ValidationState.FULL + tx.init_static_metadata_from_storage(self.tx_storage) self.validate_save(tx) def _validate_not_in_index(self, tx, index): diff --git a/tests/tx/test_verification.py b/tests/tx/test_verification.py index b3414d0b6..b3d09cc67 100644 --- a/tests/tx/test_verification.py +++ b/tests/tx/test_verification.py @@ -54,7 +54,7 @@ def _get_valid_block(self) -> Block: self._settings.GENESIS_TX2_HASH ] ) - block.update_reward_lock_metadata() + block.init_static_metadata_from_storage(self.manager.tx_storage) return block def _get_valid_merge_mined_block(self) -> MergeMinedBlock: @@ -70,7 +70,7 @@ def _get_valid_merge_mined_block(self) -> MergeMinedBlock: self._settings.GENESIS_TX2_HASH ], ) - block.update_reward_lock_metadata() + block.init_static_metadata_from_storage(self.manager.tx_storage) return block def _get_valid_tx(self) -> Transaction: @@ -95,7 +95,7 @@ def _get_valid_tx(self) -> Transaction: self._settings.GENESIS_TX2_HASH, ] ) - tx.update_reward_lock_metadata() + tx.init_static_metadata_from_storage(self.manager.tx_storage) data_to_sign = tx.get_sighash_all() assert self.manager.wallet @@ -108,7 +108,7 @@ def _get_valid_token_creation_tx(self) -> TokenCreationTransaction: add_blocks_unlock_reward(self.manager) assert self.manager.wallet tx = create_tokens(self.manager, self.manager.wallet.get_unused_address()) - tx.update_reward_lock_metadata() + tx.init_static_metadata_from_storage(self.manager.tx_storage) return tx def test_block_verify_basic(self) -> None: diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index ab87d299e..ea73f5927 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -85,6 +85,7 @@ def test_wallet_create_transaction(self): tx1.storage = self.storage tx1.update_hash() tx1.get_metadata().validation = ValidationState.FULL + tx1.init_static_metadata_from_storage(self.storage) self.storage.save_transaction(tx1) w.on_new_tx(tx1) self.assertEqual(len(w.spent_txs), 1) @@ -101,6 +102,7 @@ def test_wallet_create_transaction(self): tx2.storage = self.storage tx2.update_hash() tx2.get_metadata().validation = ValidationState.FULL + tx2.init_static_metadata_from_storage(self.storage) self.storage.save_transaction(tx2) w.on_new_tx(tx2) self.assertEqual(len(w.spent_txs), 2) @@ -206,7 +208,7 @@ def test_create_token_transaction(self): tx2.timestamp = tx.timestamp + 1 tx2.parents = self.manager.get_new_tx_parents() self.manager.cpu_mining_service.resolve(tx2) - tx2.update_reward_lock_metadata() + tx2.init_static_metadata_from_storage(self.manager.tx_storage) self.manager.verification_service.verify(tx2) self.assertNotEqual(len(tx2.inputs), 0) diff --git a/tests/wallet/test_wallet_hd.py b/tests/wallet/test_wallet_hd.py index d006b18ae..b6ad02f92 100644 --- a/tests/wallet/test_wallet_hd.py +++ b/tests/wallet/test_wallet_hd.py @@ -45,6 +45,7 @@ def test_transaction_and_balance(self): tx1.storage = self.tx_storage tx1.get_metadata().validation = ValidationState.FULL self.wallet.on_new_tx(tx1) + tx1.init_static_metadata_from_storage(self.tx_storage) self.tx_storage.save_transaction(tx1) self.assertEqual(len(self.wallet.spent_txs), 1) utxo = self.wallet.unspent_txs[self._settings.HATHOR_TOKEN_UID].get((tx1.hash, 0)) @@ -63,6 +64,7 @@ def test_transaction_and_balance(self): tx2.storage = self.tx_storage verifier.verify_script(tx=tx2, input_tx=tx2.inputs[0], spent_tx=tx1) tx2.get_metadata().validation = ValidationState.FULL + tx2.init_static_metadata_from_storage(self.tx_storage) self.tx_storage.save_transaction(tx2) self.wallet.on_new_tx(tx2) self.assertEqual(len(self.wallet.spent_txs), 2)