Skip to content

Commit

Permalink
refactor(metadata): pre-refactors for migrating feature states to sta…
Browse files Browse the repository at this point in the history
…tic metadata (#1016)
  • Loading branch information
glevco authored Aug 27, 2024
1 parent da32f80 commit 5bb5dba
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 327 deletions.
7 changes: 2 additions & 5 deletions hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,7 @@ def _get_or_create_feature_service(self) -> FeatureService:
if self._feature_service is None:
settings = self._get_or_create_settings()
tx_storage = self._get_or_create_tx_storage()
self._feature_service = FeatureService(
feature_settings=settings.FEATURE_ACTIVATION,
tx_storage=tx_storage
)
self._feature_service = FeatureService(settings=settings, tx_storage=tx_storage)

return self._feature_service

Expand All @@ -549,7 +546,7 @@ def _get_or_create_bit_signaling_service(self) -> BitSignalingService:
feature_service = self._get_or_create_feature_service()
feature_storage = self._get_or_create_feature_storage()
self._bit_signaling_service = BitSignalingService(
feature_settings=settings.FEATURE_ACTIVATION,
settings=settings,
feature_service=feature_service,
tx_storage=tx_storage,
support_features=self._support_features,
Expand Down
9 changes: 3 additions & 6 deletions hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
else:
indexes = RocksDBIndexesManager(self.rocksdb_storage)

kwargs = {}
kwargs: dict[str, Any] = {}
if not self._args.cache:
# We should only pass indexes if cache is disabled. Otherwise,
# only TransactionCacheStorage should have indexes.
Expand Down Expand Up @@ -268,13 +268,10 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
self.log.info('--x-enable-event-queue flag provided. '
'The events detected by the full node will be stored and can be retrieved by clients')

self.feature_service = FeatureService(
feature_settings=settings.FEATURE_ACTIVATION,
tx_storage=tx_storage
)
self.feature_service = FeatureService(settings=settings, tx_storage=tx_storage)

bit_signaling_service = BitSignalingService(
feature_settings=settings.FEATURE_ACTIVATION,
settings=settings,
feature_service=self.feature_service,
tx_storage=tx_storage,
support_features=self._args.signal_support,
Expand Down
2 changes: 1 addition & 1 deletion hathor/builder/resources_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def create_resources(self) -> server.Site:
(
b'feature',
FeatureResource(
feature_settings=settings.FEATURE_ACTIVATION,
settings=settings,
feature_service=self._feature_service,
tx_storage=self.manager.tx_storage
),
Expand Down
18 changes: 9 additions & 9 deletions hathor/feature_activation/bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

from structlog import get_logger

from hathor.conf.settings import HathorSettings
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.feature_service import FeatureService
from hathor.feature_activation.model.criteria import Criteria
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.feature_activation.settings import Settings as FeatureSettings
from hathor.feature_activation.storage.feature_activation_storage import FeatureActivationStorage
from hathor.transaction import Block
from hathor.transaction.storage import TransactionStorage
Expand All @@ -29,7 +29,7 @@
class BitSignalingService:
__slots__ = (
'_log',
'_feature_settings',
'_settings',
'_feature_service',
'_tx_storage',
'_support_features',
Expand All @@ -40,15 +40,15 @@ class BitSignalingService:
def __init__(
self,
*,
feature_settings: FeatureSettings,
settings: HathorSettings,
feature_service: FeatureService,
tx_storage: TransactionStorage,
support_features: set[Feature],
not_support_features: set[Feature],
feature_storage: FeatureActivationStorage | None,
) -> None:
self._log = logger.new()
self._feature_settings = feature_settings
self._settings = settings
self._feature_service = feature_service
self._tx_storage = tx_storage
self._support_features = support_features
Expand Down Expand Up @@ -163,14 +163,14 @@ 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_descriptions = self._feature_service.get_bits_description(block=block)
feature_infos = self._feature_service.get_feature_infos(block=block)
signaling_features = {
feature: description.criteria
for feature, description in feature_descriptions.items()
if description.state in FeatureState.get_signaling_states()
feature: feature_info.criteria
for feature, feature_info in feature_infos.items()
if feature_info.state in FeatureState.get_signaling_states()
}

assert len(signaling_features) <= self._feature_settings.max_signal_bits, (
assert len(signaling_features) <= self._settings.FEATURE_ACTIVATION.max_signal_bits, (
'Invalid state. Signaling more features than the allowed maximum.'
)

Expand Down
28 changes: 15 additions & 13 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, TypeAlias

from hathor.conf.settings import HathorSettings
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_description import FeatureDescription
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

if TYPE_CHECKING:
from hathor.feature_activation.bit_signaling_service import BitSignalingService
Expand All @@ -44,8 +44,8 @@ class BlockIsMissingSignal:
class FeatureService:
__slots__ = ('_feature_settings', '_tx_storage', 'bit_signaling_service')

def __init__(self, *, feature_settings: FeatureSettings, tx_storage: 'TransactionStorage') -> None:
self._feature_settings = feature_settings
def __init__(self, *, settings: HathorSettings, tx_storage: 'TransactionStorage') -> None:
self._feature_settings = settings.FEATURE_ACTIVATION
self._tx_storage = tx_storage
self.bit_signaling_service: Optional['BitSignalingService'] = None

Expand All @@ -64,11 +64,11 @@ 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
descriptions = self.get_bits_description(block=block)
feature_infos = self.get_feature_infos(block=block)

must_signal_features = (
feature for feature, description in descriptions.items()
if description.state is FeatureState.MUST_SIGNAL
feature for feature, feature_info in feature_infos.items()
if feature_info.state is FeatureState.MUST_SIGNAL
)

for feature in must_signal_features:
Expand Down Expand Up @@ -192,12 +192,12 @@ def _calculate_new_state(
if previous_state is FeatureState.FAILED:
return FeatureState.FAILED

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

def get_bits_description(self, *, block: 'Block') -> dict[Feature, FeatureDescription]:
def get_feature_infos(self, *, block: 'Block') -> dict[Feature, FeatureInfo]:
"""Returns the criteria definition and feature state for all features at a certain block."""
return {
feature: FeatureDescription(
feature: FeatureInfo(
criteria=criteria,
state=self.get_state(block=block, feature=feature)
)
Expand All @@ -223,9 +223,11 @@ def _get_ancestor_at_height(self, *, block: 'Block', ancestor_height: int) -> 'B
if parent_block.static_metadata.height == ancestor_height:
return parent_block

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)
if not parent_metadata.voided_by:
ancestor = self._tx_storage.get_block_by_height(ancestor_height)
assert ancestor is not None, (
'it is guaranteed that the ancestor of a fully connected and non-voided block is in the height index'
)
return ancestor

return self._get_ancestor_iteratively(block=parent_block, ancestor_height=ancestor_height)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from hathor.feature_activation.model.feature_state import FeatureState


class FeatureDescription(NamedTuple):
class FeatureInfo(NamedTuple):
"""Represents all information related to one feature, that is, its criteria and state."""
criteria: Criteria
state: FeatureState
9 changes: 7 additions & 2 deletions hathor/feature_activation/model/feature_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from enum import Enum
from enum import Enum, unique


class FeatureState(Enum):
@unique
class FeatureState(str, Enum):
"""
Possible states a feature can be in, for each block.
Expand All @@ -35,6 +36,10 @@ class FeatureState(Enum):
ACTIVE = 'ACTIVE'
FAILED = 'FAILED'

def is_active(self) -> bool:
"""Return whether the state is active."""
return self is FeatureState.ACTIVE

@staticmethod
def get_signaling_states() -> set['FeatureState']:
"""
Expand Down
24 changes: 13 additions & 11 deletions hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

from hathor.api_util import Resource, set_cors
from hathor.cli.openapi_files.register import register_resource
from hathor.conf.settings import HathorSettings
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.feature_service import FeatureService
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.storage import TransactionStorage
from hathor.utils.api import ErrorResponse, QueryParams, Response
Expand All @@ -36,12 +36,12 @@ class FeatureResource(Resource):
def __init__(
self,
*,
feature_settings: FeatureSettings,
settings: HathorSettings,
feature_service: FeatureService,
tx_storage: TransactionStorage
) -> None:
super().__init__()
self._feature_settings = feature_settings
self._feature_settings = settings.FEATURE_ACTIVATION
self._feature_service = feature_service
self.tx_storage = tx_storage

Expand All @@ -68,17 +68,17 @@ def get_block_features(self, request: Request) -> bytes:
return error.json_dumpb()

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

for feature, description in feature_descriptions.items():
if description.state not in FeatureState.get_signaling_states():
for feature, feature_info in feature_infos.items():
if feature_info.state not in FeatureState.get_signaling_states():
continue

block_feature = GetBlockFeatureResponse(
bit=description.criteria.bit,
signal=block.get_feature_activation_bit_value(description.criteria.bit),
bit=feature_info.criteria.bit,
signal=block.get_feature_activation_bit_value(feature_info.criteria.bit),
feature=feature,
feature_state=description.state.name
feature_state=feature_info.state.name
)

signal_bits.append(block_feature)
Expand All @@ -90,10 +90,12 @@ 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)
features = []

for feature, criteria in self._feature_settings.features.items():
state = self._feature_service.get_state(block=best_block, feature=feature)
for feature, feature_info in feature_infos.items():
state = feature_info.state
criteria = feature_info.criteria
threshold_count = criteria.get_threshold(self._feature_settings)
threshold_percentage = threshold_count / self._feature_settings.evaluation_interval
acceptance_percentage = None
Expand Down
2 changes: 2 additions & 0 deletions hathor/transaction/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
TxInput,
TxOutput,
TxVersion,
Vertex,
sum_weights,
)
from hathor.transaction.block import Block
Expand All @@ -29,6 +30,7 @@
__all__ = [
'Transaction',
'BitcoinAuxPow',
'Vertex',
'BaseTransaction',
'Block',
'MergeMinedBlock',
Expand Down
14 changes: 3 additions & 11 deletions hathor/transaction/static_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@

from __future__ import annotations

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
Expand All @@ -26,16 +24,16 @@
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.types import VertexId
from hathor.util import json_dumpb, json_loadb
from hathor.util import json_loadb
from hathor.utils.pydantic import BaseModel

if TYPE_CHECKING:
from hathor.conf.settings import HathorSettings
from hathor.transaction import BaseTransaction, Block, Transaction
from hathor.transaction.storage import TransactionStorage


@dataclass(slots=True, frozen=True, kw_only=True)
class VertexStaticMetadata(ABC):
class VertexStaticMetadata(ABC, BaseModel):
"""
Static Metadata represents vertex attributes that are not intrinsic to the vertex data, but can be calculated from
only the vertex itself and its dependencies, and whose values never change.
Expand All @@ -49,10 +47,6 @@ class VertexStaticMetadata(ABC):
# 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:
"""Convert this static metadata instance to a json bytes representation."""
return json_dumpb(dataclasses.asdict(self))

@classmethod
def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticMetadata':
"""Create a static metadata instance from a json bytes representation, with a known vertex type target."""
Expand All @@ -68,7 +62,6 @@ def from_bytes(cls, data: bytes, *, target: 'BaseTransaction') -> 'VertexStaticM
raise NotImplementedError


@dataclass(slots=True, frozen=True, kw_only=True)
class BlockStaticMetadata(VertexStaticMetadata):
height: int

Expand Down Expand Up @@ -181,7 +174,6 @@ def _get_previous_feature_activation_bit_counts(
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:
Expand Down
4 changes: 2 additions & 2 deletions hathor/transaction/storage/rocksdb_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def _save_transaction(self, tx: 'BaseTransaction', *, only_metadata: bool = Fals

@override
def _save_static_metadata(self, tx: 'BaseTransaction') -> None:
self._db.put((self._cf_static_meta, tx.hash), tx.static_metadata.to_bytes())
self._db.put((self._cf_static_meta, tx.hash), tx.static_metadata.json_dumpb())

def _load_static_metadata(self, vertex: 'BaseTransaction') -> None:
"""Set vertex static metadata loaded from what's saved in this storage."""
Expand Down Expand Up @@ -272,4 +272,4 @@ def migrate_static_metadata(self, log: BoundLogger) -> None:
)

# Save it manually to the CF
self._db.put((self._cf_static_meta, vertex_id), static_metadata.to_bytes())
self._db.put((self._cf_static_meta, vertex_id), static_metadata.json_dumpb())
5 changes: 3 additions & 2 deletions hathor/verification/block_verifier.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 typing_extensions import assert_never

from hathor.conf.settings import HathorSettings
from hathor.daa import DifficultyAdjustmentAlgorithm
from hathor.feature_activation.feature_service import BlockIsMissingSignal, BlockIsSignaling, FeatureService
Expand Down Expand Up @@ -93,5 +95,4 @@ def verify_mandatory_signaling(self, block: Block) -> None:
f"Block must signal support for feature '{feature.value}' during MUST_SIGNAL phase."
)
case _:
# TODO: This will be changed to assert_never() so mypy can check it.
raise NotImplementedError
assert_never(signaling_state)
Loading

0 comments on commit 5bb5dba

Please sign in to comment.