Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Jun 1, 2023
1 parent 41c5a9a commit 22bfb5d
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 37 deletions.
11 changes: 11 additions & 0 deletions hathor/conf/testnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ CHECKPOINTS:
1_500_000: 000000000c3591805f4748480b59ac1788f754fc004930985a487580e2b5de8f
1_600_000: 00000000060adfdfd7d488d4d510b5779cf35a3c50df7bcff941fbb6957be4d2

FEATURE_ACTIVATION:
evaluation_interval: 1000
default_threshold: 900
features:
NOP_FEATURE_1:
bit: 0
start_height: 2000
timeout_height: 4000
activate_on_timeout: true
version: 0.0.0

# TODO: Enable this config when settings via python modules are no longer used
# FEATURE_ACTIVATION:
# default_threshold: 30240 # 30240 = 75% of evaluation_interval (40320)
24 changes: 18 additions & 6 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,42 @@ 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

metadata = block.get_metadata()
feature_states = metadata.feature_states or {}

if state := feature_states.get(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
)

feature_states[feature] = new_state
metadata.feature_states = feature_states

return new_state

def _calculate_new_state(
self,
*,
Expand Down
61 changes: 33 additions & 28 deletions hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,35 +52,40 @@ def render_GET(self, request: Request) -> bytes:
bit_counts = best_block.get_feature_activation_bit_counts()
features = []

for feature, criteria in self._feature_settings.features.items():
state = self._feature_service.get_state(block=best_block, feature=feature)
threshold_count = criteria.get_threshold(self._feature_settings)
threshold_percentage = threshold_count / self._feature_settings.evaluation_interval
acceptance_percentage = None

if state is FeatureState.STARTED:
acceptance_count = bit_counts[criteria.bit]
acceptance_percentage = acceptance_count / self._feature_settings.evaluation_interval

feature_response = GetFeatureResponse(
name=feature,
state=state.name,
acceptance=acceptance_percentage,
threshold=threshold_percentage,
start_height=criteria.start_height,
minimum_activation_height=criteria.minimum_activation_height,
timeout_height=criteria.timeout_height,
activate_on_timeout=criteria.activate_on_timeout,
version=criteria.version
try:
for feature, criteria in self._feature_settings.features.items():
state = self._feature_service.get_state(block=best_block, feature=feature)
threshold_count = criteria.get_threshold(self._feature_settings)
threshold_percentage = threshold_count / self._feature_settings.evaluation_interval
acceptance_percentage = None

if state is FeatureState.STARTED:
acceptance_count = bit_counts[criteria.bit]
acceptance_percentage = acceptance_count / self._feature_settings.evaluation_interval

feature_response = GetFeatureResponse(
name=feature,
state=state.name,
acceptance=acceptance_percentage,
threshold=threshold_percentage,
start_height=criteria.start_height,
minimum_activation_height=criteria.minimum_activation_height,
timeout_height=criteria.timeout_height,
activate_on_timeout=criteria.activate_on_timeout,
version=criteria.version
)

features.append(feature_response)

response = GetFeaturesResponse(
block_hash=best_block.hash_hex,
block_height=best_block.get_height(),
features=features
)

features.append(feature_response)

response = GetFeaturesResponse(
block_hash=best_block.hash_hex,
block_height=best_block.get_height(),
features=features
)
except Exception as e:
print(e)
exit(-1)
raise

return response.json_dumpb()

Expand Down
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}

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
}

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], 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)
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

0 comments on commit 22bfb5d

Please sign in to comment.