Skip to content

Commit

Permalink
feat(feature-activation): implement caching mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Jun 5, 2023
1 parent 424a933 commit 347472d
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 54 deletions.
2 changes: 1 addition & 1 deletion hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

from hathor.conf import TESTNET_SETTINGS_FILEPATH, HathorSettings
from hathor.exception import PreInitializationError
from hathor.feature_activation.feature_service import FeatureService

logger = get_logger()
# LOGGING_CAPTURE_STDOUT = True
Expand Down Expand Up @@ -150,6 +149,7 @@ def prepare(self, args: Namespace, *, register_resources: bool = True) -> None:
from hathor.conf import HathorSettings
settings = HathorSettings()

from hathor.feature_activation.feature_service import FeatureService
feature_service = FeatureService(
feature_settings=settings.FEATURE_ACTIVATION,
tx_storage=self.manager.tx_storage
Expand Down
20 changes: 14 additions & 6 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,38 @@ def is_feature_active(self, *, block: Block, feature: Feature) -> bool:
return state == FeatureState.ACTIVE

def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
"""Returns the state of a feature at a certain block."""
"""Returns the state of a feature at a certain block. Uses block metadata to cache states."""

# per definition, the genesis block is in the DEFINED state for all features
if block.is_genesis:
return FeatureState.DEFINED

if state := block.get_feature_state(feature=feature):
return state

# 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.
# 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()
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
previous_boundary_block = self._get_ancestor_at_height(block=block, height=previous_boundary_height)
previous_state = self.get_state(block=previous_boundary_block, feature=feature)
previous_boundary_state = self.get_state(block=previous_boundary_block, feature=feature)

if offset_to_boundary != 0:
return previous_state
return previous_boundary_state

return self._calculate_new_state(
new_state = self._calculate_new_state(
boundary_block=block,
feature=feature,
previous_state=previous_state
previous_state=previous_boundary_state
)

block.update_feature_state(feature=feature, state=new_state)

return new_state

def _calculate_new_state(
self,
*,
Expand Down
11 changes: 9 additions & 2 deletions hathor/simulator/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from hathor.simulator.miner.geometric_miner import GeometricMiner
from hathor.simulator.tx_generator import RandomTransactionGenerator
from hathor.transaction.genesis import _get_genesis_transactions_unsafe
from hathor.transaction.storage import TransactionStorage
from hathor.util import Random
from hathor.wallet import HDWallet

Expand Down Expand Up @@ -149,7 +150,8 @@ def create_peer(
enable_sync_v2: bool = True,
soft_voided_tx_ids: Optional[Set[bytes]] = None,
full_verification: bool = True,
event_ws_factory: Optional[EventWebsocketFactory] = None
event_ws_factory: Optional[EventWebsocketFactory] = None,
tx_storage: Optional[TransactionStorage] = None
) -> HathorManager:
artifacts = self.create_peer_artifacts(
network=network,
Expand All @@ -159,6 +161,7 @@ def create_peer(
soft_voided_tx_ids=soft_voided_tx_ids,
full_verification=full_verification,
event_ws_factory=event_ws_factory,
tx_storage=tx_storage
)

return artifacts.manager
Expand All @@ -171,7 +174,8 @@ def create_peer_artifacts(
enable_sync_v2: bool = True,
soft_voided_tx_ids: Optional[Set[bytes]] = None,
full_verification: bool = True,
event_ws_factory: Optional[EventWebsocketFactory] = None
event_ws_factory: Optional[EventWebsocketFactory] = None,
tx_storage: Optional[TransactionStorage] = None
) -> BuildArtifacts:
assert self._started, 'Simulator is not started.'
assert peer_id is not None # XXX: temporary, for checking that tests are using the peer_id
Expand All @@ -191,6 +195,9 @@ def create_peer_artifacts(
.set_soft_voided_tx_ids(soft_voided_tx_ids or set()) \
.use_memory()

if tx_storage:
builder.set_tx_storage(tx_storage)

if event_ws_factory:
builder.enable_event_manager(event_ws_factory=event_ws_factory)

Expand Down
19 changes: 19 additions & 0 deletions hathor/transaction/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from hathor import daa
from hathor.checkpoint import Checkpoint
from hathor.conf import HathorSettings
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.exceptions import (
Expand Down Expand Up @@ -415,3 +417,20 @@ def _get_feature_activation_bitmask(cls) -> int:
bitmask = (1 << settings.FEATURE_ACTIVATION.max_signal_bits) - 1

return bitmask

def get_feature_state(self, *, feature: Feature) -> Optional[FeatureState]:
"""Returns the state of a feature from metadata."""
metadata = self.get_metadata()
feature_states = metadata.feature_states or {}

return feature_states.get(feature)

def update_feature_state(self, *, feature: Feature, state: FeatureState) -> None:
"""Updates the state of a feature in metadata and persists it."""
assert self.storage is not None
metadata = self.get_metadata()
feature_states = metadata.feature_states or {}
feature_states[feature] = state
metadata.feature_states = feature_states

self.storage.save_transaction(self, only_metadata=True)
23 changes: 20 additions & 3 deletions hathor/transaction/transaction_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Set

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.util import practically_equal

Expand Down Expand Up @@ -49,6 +51,10 @@ class TransactionMetadata:
# 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.
feature_states: Optional[dict[Feature, FeatureState]] = None

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

Expand Down Expand Up @@ -181,9 +187,9 @@ def __eq__(self, other: Any) -> bool:
"""Override the default Equals behavior"""
if not isinstance(other, TransactionMetadata):
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']:
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']:
if (getattr(self, field) or None) != (getattr(other, field) or None):
return False

Expand Down Expand Up @@ -219,6 +225,10 @@ 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

if self.feature_states is not None:
data['feature_states'] = {feature.value: state.value for feature, state in self.feature_states.items()}

if self.first_block is not None:
data['first_block'] = self.first_block.hex()
else:
Expand Down Expand Up @@ -270,6 +280,13 @@ def create_from_json(cls, data: Dict[str, Any]) -> 'TransactionMetadata':
meta.min_height = data.get('min_height', 0)
meta.feature_activation_bit_counts = data.get('feature_activation_bit_counts', [])

feature_states_raw = data.get('feature_states')
if feature_states_raw:
meta.feature_states = {
Feature(feature): FeatureState(feature_state)
for feature, feature_state in feature_states_raw.items()
}

first_block_raw = data.get('first_block', None)
if first_block_raw:
meta.first_block = bytes.fromhex(first_block_raw)
Expand Down
30 changes: 30 additions & 0 deletions tests/feature_activation/test_feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,36 @@ def test_get_state_from_active(block_mocks: list[Block], tx_storage: Transaction
assert result == FeatureState.ACTIVE


@pytest.mark.parametrize('block_height', [12, 13, 14, 15])
def test_caching_mechanism(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None:
feature_settings = FeatureSettings.construct(
evaluation_interval=4,
features={
Feature.NOP_FEATURE_1: Criteria.construct(
bit=Mock(),
start_height=0,
timeout_height=4,
activate_on_timeout=True,
version=Mock()
)
}
)
service = FeatureService(feature_settings=feature_settings, tx_storage=tx_storage)
block = block_mocks[block_height]
calculate_new_state_mock = Mock(wraps=service._calculate_new_state)

with patch.object(FeatureService, '_calculate_new_state', calculate_new_state_mock):
result1 = service.get_state(block=block, feature=Feature.NOP_FEATURE_1)

assert result1 == FeatureState.ACTIVE
assert calculate_new_state_mock.call_count == 3

result2 = service.get_state(block=block, feature=Feature.NOP_FEATURE_1)

assert result2 == FeatureState.ACTIVE
assert calculate_new_state_mock.call_count == 3


@pytest.mark.parametrize('block_height', [12, 13, 14, 15])
def test_is_feature_active(block_mocks: list[Block], tx_storage: TransactionStorage, block_height: int) -> None:
feature_settings = FeatureSettings.construct(
Expand Down
Loading

0 comments on commit 347472d

Please sign in to comment.