Skip to content

Commit

Permalink
feat(feature-activation): implement closest_block metadata and Featur…
Browse files Browse the repository at this point in the history
…e Activation for Transactions (#933)
  • Loading branch information
glevco authored Nov 7, 2024
1 parent 487e731 commit bfeacb5
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 56 deletions.
2 changes: 1 addition & 1 deletion hathor/feature_activation/bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def _log_signal_bits(self, feature: Feature, enable_bit: bool, support: bool, no

def _get_signaling_features(self, block: Block) -> dict[Feature, Criteria]:
"""Given a specific block, return all features that are in a signaling state for that block."""
feature_infos = self._feature_service.get_feature_infos(block=block)
feature_infos = self._feature_service.get_feature_infos(vertex=block)
signaling_features = {
feature: feature_info.criteria
for feature, feature_info in feature_infos.items()
Expand Down
34 changes: 27 additions & 7 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, TypeAlias

Expand All @@ -22,7 +24,7 @@

if TYPE_CHECKING:
from hathor.feature_activation.bit_signaling_service import BitSignalingService
from hathor.transaction import Block
from hathor.transaction import Block, Vertex
from hathor.transaction.storage import TransactionStorage


Expand All @@ -49,11 +51,20 @@ def __init__(self, *, settings: HathorSettings, tx_storage: 'TransactionStorage'
self._tx_storage = tx_storage
self.bit_signaling_service: Optional['BitSignalingService'] = None

def is_feature_active(self, *, block: 'Block', feature: Feature) -> bool:
"""Returns whether a Feature is active at a certain block."""
def is_feature_active(self, *, vertex: Vertex, feature: Feature) -> bool:
"""Return whether a Feature is active for a certain vertex."""
block = self._get_feature_activation_block(vertex)
state = self.get_state(block=block, feature=feature)
return state.is_active()

return state == FeatureState.ACTIVE
def _get_feature_activation_block(self, vertex: Vertex) -> Block:
"""Return the block used for feature activation depending on the vertex type."""
from hathor.transaction import Block, Transaction
if isinstance(vertex, Block):
return vertex
if isinstance(vertex, Transaction):
return self._tx_storage.get_block(vertex.static_metadata.closest_ancestor_block)
raise NotImplementedError

def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState:
"""
Expand All @@ -64,7 +75,7 @@ def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState
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
feature_infos = self.get_feature_infos(block=block)
feature_infos = self.get_feature_infos(vertex=block)

must_signal_features = (
feature for feature, feature_info in feature_infos.items()
Expand Down Expand Up @@ -194,8 +205,9 @@ def _calculate_new_state(

raise NotImplementedError(f'Unknown previous state: {previous_state}')

def get_feature_infos(self, *, block: 'Block') -> dict[Feature, FeatureInfo]:
"""Returns the criteria definition and feature state for all features at a certain block."""
def get_feature_infos(self, *, vertex: Vertex) -> dict[Feature, FeatureInfo]:
"""Return the criteria definition and feature state for all features for a certain vertex."""
block = self._get_feature_activation_block(vertex)
return {
feature: FeatureInfo(
criteria=criteria,
Expand All @@ -204,6 +216,14 @@ def get_feature_infos(self, *, block: 'Block') -> dict[Feature, FeatureInfo]:
for feature, criteria in self._feature_settings.features.items()
}

def get_feature_states(self, *, vertex: Vertex) -> dict[Feature, FeatureState]:
"""Return the feature state for all features for a certain vertex."""
feature_infos = self.get_feature_infos(vertex=vertex)
return {
feature: info.state
for feature, info in feature_infos.items()
}

def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'Block':
"""
Given a block, return its ancestor at a specific height.
Expand Down
4 changes: 2 additions & 2 deletions hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def get_block_features(self, request: Request) -> bytes:
return error.json_dumpb()

signal_bits = []
feature_infos = self._feature_service.get_feature_infos(block=block)
feature_infos = self._feature_service.get_feature_infos(vertex=block)

for feature, feature_info in feature_infos.items():
if feature_info.state not in FeatureState.get_signaling_states():
Expand All @@ -90,7 +90,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.static_metadata.feature_activation_bit_counts
feature_infos = self._feature_service.get_feature_infos(block=best_block)
feature_infos = self._feature_service.get_feature_infos(vertex=best_block)
features = []

for feature, feature_info in feature_infos.items():
Expand Down
61 changes: 54 additions & 7 deletions hathor/transaction/static_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from operator import add
from typing import TYPE_CHECKING, Callable

from typing_extensions import Self
from typing_extensions import Self, override

from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_state import FeatureState
Expand Down Expand Up @@ -57,6 +57,7 @@ def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticM
return BlockStaticMetadata(**json_dict)

if isinstance(target, Transaction):
json_dict['closest_ancestor_block'] = bytes.fromhex(json_dict['closest_ancestor_block'])
return TransactionStaticMetadata(**json_dict)

raise NotImplementedError
Expand Down Expand Up @@ -175,6 +176,10 @@ def _get_previous_feature_activation_bit_counts(


class TransactionStaticMetadata(VertexStaticMetadata):
# The Block with the greatest height that is a direct or indirect dependency (ancestor) of the transaction,
# including both funds and verification DAGs. It's used by Feature Activation for Transactions.
closest_ancestor_block: VertexId

@classmethod
def create_from_storage(cls, tx: 'Transaction', settings: HathorSettings, storage: 'TransactionStorage') -> Self:
"""Create a `TransactionStaticMetadata` using dependencies provided by a storage."""
Expand All @@ -189,14 +194,12 @@ def create(
) -> Self:
"""Create a `TransactionStaticMetadata` using dependencies provided by a `vertex_getter`.
This must be fast, ideally O(1)."""
min_height = cls._calculate_min_height(
tx,
settings,
vertex_getter=vertex_getter,
)
min_height = cls._calculate_min_height(tx, settings, vertex_getter)
closest_ancestor_block = cls._calculate_closest_ancestor_block(tx, settings, vertex_getter)

return cls(
min_height=min_height
min_height=min_height,
closest_ancestor_block=closest_ancestor_block,
)

@classmethod
Expand Down Expand Up @@ -245,3 +248,47 @@ def _calculate_my_min_height(
if isinstance(spent_tx, Block):
min_height = max(min_height, spent_tx.static_metadata.height + settings.REWARD_SPEND_MIN_BLOCKS + 1)
return min_height

@staticmethod
def _calculate_closest_ancestor_block(
tx: 'Transaction',
settings: HathorSettings,
vertex_getter: Callable[[VertexId], 'BaseTransaction'],
) -> VertexId:
"""
Calculate the tx's closest_ancestor_block. It's calculated by propagating the metadata forward in the DAG.
"""
from hathor.transaction import Block, Transaction
if tx.is_genesis:
return settings.GENESIS_BLOCK_HASH

closest_ancestor_block: Block | None = None

for vertex_id in tx.get_all_dependencies():
vertex = vertex_getter(vertex_id)
candidate_block: Block

if isinstance(vertex, Block):
candidate_block = vertex
elif isinstance(vertex, Transaction):
vertex_candidate = vertex_getter(vertex.static_metadata.closest_ancestor_block)
assert isinstance(vertex_candidate, Block)
candidate_block = vertex_candidate
else:
raise NotImplementedError

if (
not closest_ancestor_block
or candidate_block.static_metadata.height > closest_ancestor_block.static_metadata.height
):
closest_ancestor_block = candidate_block

assert closest_ancestor_block is not None
return closest_ancestor_block.hash

@override
def json_dumpb(self) -> bytes:
from hathor.util import json_dumpb
json_dict = self.dict()
json_dict['closest_ancestor_block'] = json_dict['closest_ancestor_block'].hex()
return json_dumpb(json_dict)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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 structlog import get_logger

from hathor.transaction.storage.migrations import BaseMigration

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 'add_closest_ancestor_block'

def run(self, storage: 'TransactionStorage') -> None:
raise Exception('Cannot migrate your database due to an incompatible change in the metadata. '
'Please, delete your data folder and use the latest available snapshot or sync '
'from beginning.')
2 changes: 1 addition & 1 deletion hathor/verification/merge_mined_block_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def verify_aux_pow(self, block: MergeMinedBlock) -> None:
assert block.aux_pow is not None

is_feature_active = self._feature_service.is_feature_active(
block=block,
vertex=block,
feature=Feature.INCREASE_MAX_MERKLE_PATH_LENGTH
)
max_merkle_path_length = (
Expand Down
16 changes: 7 additions & 9 deletions hathor/vertex_handler/vertex_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,24 +208,22 @@ def _log_new_object(self, tx: BaseTransaction, message_fmt: str, *, quiet: bool)
"""
metadata = tx.get_metadata()
now = datetime.datetime.fromtimestamp(self._reactor.seconds())
feature_states = self._feature_service.get_feature_states(vertex=tx)
kwargs = {
'tx': tx,
'ts_date': datetime.datetime.fromtimestamp(tx.timestamp),
'time_from_now': tx.get_time_from_now(now),
'validation': metadata.validation.name,
'feature_states': {
feature.value: state.value
for feature, state in feature_states.items()
}
}
if self._log_vertex_bytes:
kwargs['bytes'] = bytes(tx).hex()
if tx.is_block:
if isinstance(tx, Block):
message = message_fmt.format('block')
if isinstance(tx, Block):
feature_infos = self._feature_service.get_feature_infos(block=tx)
feature_states = {
feature.value: info.state.value
for feature, info in feature_infos.items()
}
kwargs['_height'] = tx.get_height()
kwargs['feature_states'] = feature_states
kwargs['_height'] = tx.get_height()
else:
message = message_fmt.format('tx')
if not quiet:
Expand Down
8 changes: 4 additions & 4 deletions tests/feature_activation/test_bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from hathor.feature_activation.model.feature_info import FeatureInfo
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.feature_activation.settings import Settings as FeatureSettings
from hathor.transaction import Block
from hathor.transaction import Block, Vertex
from hathor.transaction.storage import TransactionStorage


Expand Down Expand Up @@ -169,7 +169,7 @@ def _test_generate_signal_bits(
settings = Mock(spec_set=HathorSettings)
settings.FEATURE_ACTIVATION = FeatureSettings()
feature_service = Mock(spec_set=FeatureService)
feature_service.get_feature_infos = lambda block: feature_infos
feature_service.get_feature_infos = lambda vertex: feature_infos

service = BitSignalingService(
settings=settings,
Expand Down Expand Up @@ -264,8 +264,8 @@ def test_non_signaling_features_warning(
tx_storage = Mock(spec_set=TransactionStorage)
tx_storage.get_best_block = lambda: best_block

def get_feature_infos_mock(block: Block) -> dict[Feature, FeatureInfo]:
if block == best_block:
def get_feature_infos_mock(vertex: Vertex) -> dict[Feature, FeatureInfo]:
if vertex == best_block:
return {}
raise NotImplementedError

Expand Down
4 changes: 2 additions & 2 deletions tests/feature_activation/test_feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def test_is_feature_active(block_height: int) -> None:
service.bit_signaling_service = Mock()
block = not_none(storage.get_block_by_height(block_height))

result = service.is_feature_active(block=block, feature=Feature.NOP_FEATURE_1)
result = service.is_feature_active(vertex=block, feature=Feature.NOP_FEATURE_1)

assert result is True

Expand Down Expand Up @@ -505,7 +505,7 @@ def get_state(self: FeatureService, *, block: Block, feature: Feature) -> Featur
return states[feature]

with patch('hathor.feature_activation.feature_service.FeatureService.get_state', get_state):
result = service.get_feature_infos(block=Mock())
result = service.get_feature_infos(vertex=Mock(spec_set=Block))

expected = {
Feature.NOP_FEATURE_1: FeatureInfo(criteria_mock_1, FeatureState.STARTED),
Expand Down
Loading

0 comments on commit bfeacb5

Please sign in to comment.