Skip to content

Commit

Permalink
feat(feature-activation): implement get endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed May 30, 2023
1 parent 5cf821f commit fddc093
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 7 deletions.
12 changes: 12 additions & 0 deletions hathor/builder/resources_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def create_resources(self, args: Namespace) -> server.Site:
DebugRaiseResource,
DebugRejectResource,
)
from hathor.feature_activation.feature_service import FeatureService
from hathor.feature_activation.resources.feature import FeatureResource
from hathor.mining.ws import MiningWebsocketFactory
from hathor.p2p.resources import (
AddPeersResource,
Expand Down Expand Up @@ -189,6 +191,16 @@ def create_resources(self, args: Namespace) -> server.Site:
(b'peers', AddPeersResource(self.manager), p2p_resource),
(b'netfilter', NetfilterRuleResource(self.manager), p2p_resource),
(b'readiness', HealthcheckReadinessResource(self.manager), p2p_resource),
# Feature Activation
(
b'feature',
FeatureResource(
settings=settings.FEATURE_ACTIVATION,
feature_service=FeatureService(settings=settings.FEATURE_ACTIVATION),
tx_storage=self.manager.tx_storage
),
root
)
]
# XXX: only enable UTXO search API if the index is enabled
if args.utxo_index:
Expand Down
3 changes: 2 additions & 1 deletion hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def _calculate_new_state(

# Get the count for this block's parent. Since this is a boundary block, its parent count represents the
# previous evaluation interval count.
counts = boundary_block.get_parent_feature_activation_bit_counts()
parent_block = boundary_block.get_block_parent()
counts = parent_block.get_feature_activation_bit_counts()
count = counts[criteria.bit]
threshold = criteria.threshold if criteria.threshold is not None else self._settings.default_threshold

Expand Down
Empty file.
144 changes: 144 additions & 0 deletions hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 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 Optional

from hathor.api_util import Resource, set_cors
from hathor.cli.openapi_files.register import register_resource
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
from hathor.transaction.storage import TransactionStorage
from hathor.utils.api import Response


@register_resource
class FeatureResource(Resource):
__slots__ = ()

isLeaf = True

def __init__(
self,
*,
settings: Settings,
feature_service: FeatureService,
tx_storage: TransactionStorage
) -> None:
super().__init__()
self._settings = settings
self._feature_service = feature_service
self.tx_storage = tx_storage

def render_GET(self, request):
request.setHeader(b'content-type', b'application/json; charset=utf-8')
set_cors(request, 'GET')

best_block = self.tx_storage.get_best_block()
bit_counts = best_block.get_feature_activation_bit_counts()
features = []

for feature, criteria in self._settings.features.items():
state = self._feature_service.get_state(block=best_block, feature=feature)
acceptance_count = bit_counts[criteria.bit]
acceptance = (
acceptance_count / self._settings.evaluation_interval if state is FeatureState.STARTED else None
)
threshold_count = (
criteria.threshold if criteria.threshold is not None else self._settings.default_threshold
)

feature_response = GetFeatureResponse(
name=feature,
state=state.name,
acceptance=acceptance,
threshold=threshold_count / self._settings.evaluation_interval,
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(features=features)

return response.json_dumpb()


class GetFeatureResponse(Response, use_enum_values=True):
name: Feature
state: str
acceptance: Optional[float]
threshold: float
start_height: int
minimum_activation_height: int
timeout_height: int
activate_on_timeout: bool
version: str


class GetFeaturesResponse(Response):
features: list[GetFeatureResponse]


FeatureResource.openapi = {
'/feature': {
'x-visibility': 'private',
'get': {
'operationId': 'feature',
'summary': 'Feature Activation',
'description': 'Returns information about features in the Feature Activation process',
'responses': {
'200': {
'description': 'Success',
'content': {
'application/json': {
'examples': {
'success': {
'features': [
{
'name': 'NOP_FEATURE_1',
'state': 'ACTIVE',
'acceptance': None,
'threshold': 0.75,
'start_height': 0,
'minimum_activation_height': 0,
'timeout_height': 100,
'activate_on_timeout': False,
'version': '0.1.0'
},
{
'name': 'NOP_FEATURE_2',
'state': 'STARTED',
'acceptance': 0.25,
'threshold': 0.5,
'start_height': 200,
'minimum_activation_height': 0,
'timeout_height': 300,
'activate_on_timeout': False,
'version': '0.2.0'
}
]
}
}
}
}
}
}
}
}
}
14 changes: 8 additions & 6 deletions hathor/transaction/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ def _get_previous_feature_activation_bit_counts(self) -> list[int]:
if is_boundary_block:
return []

return self.get_parent_feature_activation_bit_counts()
parent_block = self.get_block_parent()

return parent_block.get_feature_activation_bit_counts()

def get_next_block_best_chain_hash(self) -> Optional[bytes]:
"""Return the hash of the next (child/left-to-right) block in the best blockchain.
Expand Down Expand Up @@ -386,12 +388,12 @@ def get_height(self) -> int:
"""Returns the block's height."""
return self.get_metadata().height

def get_parent_feature_activation_bit_counts(self) -> list[int]:
"""Returns the parent block's feature_activation_bit_counts metadata attribute."""
parent_metadata = self.get_block_parent().get_metadata()
assert parent_metadata.feature_activation_bit_counts is not None, 'Blocks must always have this attribute set.'
def get_feature_activation_bit_counts(self) -> list[int]:
"""Returns the block's feature_activation_bit_counts metadata attribute."""
metadata = self.get_metadata()
assert metadata.feature_activation_bit_counts is not None, 'Blocks must always have this attribute set.'

return parent_metadata.feature_activation_bit_counts
return metadata.feature_activation_bit_counts

def _get_feature_activation_bit_list(self) -> list[int]:
"""
Expand Down
Empty file.
103 changes: 103 additions & 0 deletions tests/resources/feature/test_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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 unittest.mock import Mock

import pytest

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.resources.feature import FeatureResource
from hathor.feature_activation.settings import Settings
from hathor.transaction import Block
from hathor.transaction.storage import TransactionStorage
from tests.resources.base_resource import StubSite


@pytest.fixture
def web():
best_block = Mock(spec_set=Block)
best_block.get_feature_activation_bit_counts = Mock(return_value=[0, 1, 0, 0])

tx_storage = Mock(spec_set=TransactionStorage)
tx_storage.get_best_block = Mock(return_value=best_block)

def get_state(*, block: Block, feature: Feature) -> FeatureState:
return FeatureState.ACTIVE if feature is Feature.NOP_FEATURE_1 else FeatureState.STARTED

feature_service = Mock(spec_set=FeatureService)
feature_service.get_state = Mock(side_effect=get_state)

settings = Settings(
evaluation_interval=4,
default_threshold=3,
features={
Feature.NOP_FEATURE_1: Criteria(
bit=0,
start_height=0,
timeout_height=100,
version='0.1.0'
),
Feature.NOP_FEATURE_2: Criteria(
bit=1,
start_height=200,
threshold=2,
timeout_height=300,
version='0.2.0'
)
}
)

feature_resource = FeatureResource(
settings=settings,
feature_service=feature_service,
tx_storage=tx_storage
)

return StubSite(feature_resource)


def test_get_features(web):
response = web.get('feature')
result = response.result.json_value()
expected = dict(
features=[
dict(
name='NOP_FEATURE_1',
state='ACTIVE',
acceptance=None,
threshold=0.75,
start_height=0,
minimum_activation_height=0,
timeout_height=100,
activate_on_timeout=False,
version='0.1.0'
),
dict(
name='NOP_FEATURE_2',
state='STARTED',
acceptance=0.25,
threshold=0.5,
start_height=200,
minimum_activation_height=0,
timeout_height=300,
activate_on_timeout=False,
version='0.2.0'
)
]
)

assert result == expected

0 comments on commit fddc093

Please sign in to comment.