-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(feature-activation): implement closest_block metadata and Feature Activation for Transactions #933
base: master
Are you sure you want to change the base?
feat(feature-activation): implement closest_block metadata and Feature Activation for Transactions #933
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -707,6 +707,8 @@ def update_initial_metadata(self, *, save: bool = True) -> None: | |
self._update_height_metadata() | ||
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) | ||
|
@@ -732,6 +734,60 @@ 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_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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check if |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd name it |
||
elif isinstance(vertex, Transaction): | ||
this_closest_block_id = ( | ||
self._settings.GENESIS_BLOCK_HASH if vertex.is_genesis else not_none(vertex_meta.closest_block) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you can safely do |
||
) | ||
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 | ||
|
||
|
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() |
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_feature_activation_bit_counts_metadata2' | ||
|
||
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_block: | ||
vertex.update_initial_metadata() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd name it |
||
|
||
# It must be a weakref. | ||
_tx_ref: Optional['ReferenceType[BaseTransaction]'] | ||
|
||
|
@@ -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 | ||
|
||
|
@@ -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: | ||
|
@@ -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 | ||
|
||
|
@@ -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()} | ||
|
@@ -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 | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert that
isinstance(tx, Transaction)
to prevent bugs if mypy fails to detect wrong call types.