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
  • Loading branch information
glevco committed Jan 23, 2024
1 parent 9ef629f commit cfe1640
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 8 deletions.
14 changes: 11 additions & 3 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
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.util import not_none

if TYPE_CHECKING:
from hathor.transaction import Block
from hathor.transaction import Block, Transaction
from hathor.transaction.storage import TransactionStorage


Expand All @@ -47,8 +48,15 @@ def __init__(self, *, feature_settings: FeatureSettings, tx_storage: 'Transactio
self._feature_settings = feature_settings
self._tx_storage = tx_storage

def is_feature_active(self, *, block: 'Block', feature: Feature) -> bool:
"""Returns whether a Feature is active at a certain block."""
def is_feature_active_for_transaction(self, *, tx: 'Transaction', feature: Feature) -> bool:
"""Return whether a Feature is active for a certain Transaction."""
metadata = tx.get_metadata()
closest_block = self._tx_storage.get_block(not_none(metadata.closest_block))

return self.is_feature_active_for_block(block=closest_block, feature=feature)

def is_feature_active_for_block(self, *, block: 'Block', feature: Feature) -> bool:
"""Return whether a Feature is active for a certain block."""
state = self.get_state(block=block, feature=feature)

return state == FeatureState.ACTIVE
Expand Down
2 changes: 1 addition & 1 deletion hathor/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,7 @@ def _log_feature_states(self, vertex: BaseTransaction) -> None:

def _log_if_feature_is_active(self, block: Block, feature: Feature) -> None:
"""Log if a feature is ACTIVE for a block. Used as part of the Feature Activation Phased Testing."""
if self._feature_service.is_feature_active(block=block, feature=feature):
if self._feature_service.is_feature_active_for_block(block=block, feature=feature):
self.log.info(
'Feature is ACTIVE for block',
feature=feature.value,
Expand Down
48 changes: 47 additions & 1 deletion hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from hathor.transaction.util import VerboseCallback, int_to_bytes, unpack, unpack_len
from hathor.transaction.validation_state import ValidationState
from hathor.types import TokenUid, TxOutputScript, VertexId
from hathor.util import classproperty
from hathor.util import classproperty, not_none

if TYPE_CHECKING:
from _hashlib import HASH
Expand Down Expand Up @@ -708,6 +708,7 @@ def update_initial_metadata(self, *, save: bool = True) -> None:
self._update_parents_children_metadata()
self._update_reward_lock_metadata()
self._update_feature_activation_bit_counts()
self._update_closest_block_metadata()
if save:
assert self.storage is not None
self.storage.save_transaction(self, only_metadata=True)
Expand Down Expand Up @@ -742,6 +743,51 @@ def _update_feature_activation_bit_counts(self) -> None:
# This method lazily calculates and stores the value in metadata
self.get_feature_activation_bit_counts()

def _update_closest_block_metadata(self) -> None:
"""
Set the tx's closest_block metadata.
For blocks, it's always None. For Transactions, it's the Block with the greatest height that is a direct
or indirect dependency (ancestor) of the transaction, including both funds and confirmation DAGs.
It's calculated by propagating the metadata forward in the DAG,
and it's used by Feature Activation for Transactions.
"""
from hathor.transaction import Block, Transaction
if isinstance(self, Block):
return
assert isinstance(self, Transaction)
assert self.storage is not None
metadata = self.get_metadata()

if self.is_genesis:
metadata.closest_block = self._settings.GENESIS_BLOCK_HASH
return

closest_block: Block | None = None
dependency_ids = self.parents + [tx_input.tx_id for tx_input in self.inputs]

for vertex_id in dependency_ids:
vertex = self.storage.get_transaction(vertex_id)
vertex_meta = vertex.get_metadata()
this_closest_block: Block

if isinstance(vertex, Block):
assert vertex_meta.closest_block is None
this_closest_block = vertex
elif isinstance(vertex, Transaction):
this_closest_block_id = (
self._settings.GENESIS_BLOCK_HASH if vertex.is_genesis else not_none(vertex_meta.closest_block)
)
this_closest_block = self.storage.get_block(this_closest_block_id)
else:
raise NotImplementedError

if not closest_block or (this_closest_block.get_height() > closest_block.get_height()):
closest_block = this_closest_block

assert closest_block is not None
assert closest_block.hash is not None
metadata.closest_block = closest_block.hash

def update_timestamp(self, now: int) -> None:
"""Update this tx's timestamp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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
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 'add_closest_block_metadata'

def run(self, storage: 'TransactionStorage') -> None:
log = logger.new()
topological_iterator = storage.topological_iterator()

for vertex in progress(topological_iterator, log=log, total=None):
if vertex.is_transaction:
vertex.update_initial_metadata()
2 changes: 2 additions & 0 deletions hathor/transaction/storage/transaction_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from hathor.transaction.storage.migrations import (
BaseMigration,
MigrationState,
add_closest_block_metadata,
add_feature_activation_bit_counts_metadata,
add_feature_activation_bit_counts_metadata2,
add_min_height_metadata,
Expand Down Expand Up @@ -92,6 +93,7 @@ class TransactionStorage(ABC):
add_feature_activation_bit_counts_metadata.Migration,
remove_first_nop_features.Migration,
add_feature_activation_bit_counts_metadata2.Migration,
add_closest_block_metadata.Migration,
]

_migrations: list[BaseMigration]
Expand Down
18 changes: 16 additions & 2 deletions hathor/transaction/transaction_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.transaction.validation_state import ValidationState
from hathor.types import VertexId
from hathor.util import practically_equal

if TYPE_CHECKING:
Expand Down Expand Up @@ -56,6 +57,12 @@ class TransactionMetadata:
# is None otherwise. This is only used for caching, so it can be safely cleared up, as it would be recalculated
# when necessary.
feature_states: Optional[dict[Feature, FeatureState]] = None

# For Blocks, this is None. For Transactions, this is the Block with the greatest height that is a direct or
# indirect dependency (ancestor) of the transaction, including both funds and confirmation DAGs.
# It's used by Feature Activation for Transactions.
closest_block: VertexId | None

# It must be a weakref.
_tx_ref: Optional['ReferenceType[BaseTransaction]']

Expand All @@ -71,7 +78,8 @@ def __init__(
score: float = 0,
height: Optional[int] = None,
min_height: Optional[int] = None,
feature_activation_bit_counts: Optional[list[int]] = None
feature_activation_bit_counts: Optional[list[int]] = None,
closest_block: VertexId | None = None,
) -> None:
from hathor.transaction.genesis import is_genesis

Expand Down Expand Up @@ -129,6 +137,8 @@ def __init__(

self.feature_activation_bit_counts = feature_activation_bit_counts

self.closest_block = closest_block

settings = get_global_settings()

# Genesis specific:
Expand Down Expand Up @@ -192,7 +202,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']:
'min_height', 'feature_activation_bit_counts', 'feature_states', 'closest_block']:
if (getattr(self, field) or None) != (getattr(other, field) or None):
return False

Expand Down Expand Up @@ -228,6 +238,7 @@ def to_json(self) -> dict[str, Any]:
data['height'] = self.height
data['min_height'] = self.min_height
data['feature_activation_bit_counts'] = self.feature_activation_bit_counts
data['closest_block'] = self.closest_block.hex() if self.closest_block is not None else None

if self.feature_states is not None:
data['feature_states'] = {feature.value: state.value for feature, state in self.feature_states.items()}
Expand Down Expand Up @@ -292,6 +303,9 @@ def create_from_json(cls, data: dict[str, Any]) -> 'TransactionMetadata':
if first_block_raw:
meta.first_block = bytes.fromhex(first_block_raw)

closest_block_raw = data.get('closest_block')
meta.closest_block = bytes.fromhex(closest_block_raw) if closest_block_raw is not None else None

_val_name = data.get('validation', None)
meta.validation = ValidationState.from_name(_val_name) if _val_name is not None else ValidationState.INITIAL

Expand Down
2 changes: 1 addition & 1 deletion tests/feature_activation/test_feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStor
)
block = block_mocks[block_height]

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

assert result is True

Expand Down

0 comments on commit cfe1640

Please sign in to comment.