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 Jun 14, 2023
1 parent f772aac commit f3f3d19
Show file tree
Hide file tree
Showing 13 changed files with 358 additions and 52 deletions.
8 changes: 8 additions & 0 deletions hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from hathor.event import EventManager
from hathor.event.storage import EventMemoryStorage, EventRocksDBStorage, EventStorage
from hathor.event.websocket import EventWebsocketFactory
from hathor.feature_activation.feature_service import FeatureService
from hathor.indexes import IndexesManager, MemoryIndexesManager, RocksDBIndexesManager
from hathor.manager import HathorManager
from hathor.p2p.manager import ConnectionsManager
Expand Down Expand Up @@ -63,6 +64,7 @@ class BuildArtifacts(NamedTuple):
wallet: Optional[BaseWallet]
rocksdb_storage: Optional[RocksDBStorage]
stratum_factory: Optional[StratumFactory]
feature_service: FeatureService


class Builder:
Expand Down Expand Up @@ -189,6 +191,8 @@ def build(self) -> BuildArtifacts:
if self._enable_stratum_server:
stratum_factory = self._create_stratum_server(manager)

feature_service = self._create_feature_service()

self.artifacts = BuildArtifacts(
peer_id=peer_id,
settings=settings,
Expand All @@ -203,6 +207,7 @@ def build(self) -> BuildArtifacts:
wallet=wallet,
rocksdb_storage=self._rocksdb_storage,
stratum_factory=stratum_factory,
feature_service=feature_service
)

return self.artifacts
Expand Down Expand Up @@ -268,6 +273,9 @@ def _create_stratum_server(self, manager: HathorManager) -> StratumFactory:
manager.metrics.stratum_factory = stratum_factory
return stratum_factory

def _create_feature_service(self) -> FeatureService:
return FeatureService(feature_settings=self._settings.FEATURE_ACTIVATION)

def _get_or_create_rocksdb_storage(self) -> RocksDBStorage:
assert self._rocksdb_path is not None

Expand Down
21 changes: 20 additions & 1 deletion hathor/builder/resources_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from hathor.event.resources.event import EventResource
from hathor.exception import BuilderError
from hathor.feature_activation.feature_service import FeatureService
from hathor.prometheus import PrometheusMetricsExporter

if TYPE_CHECKING:
Expand All @@ -33,7 +34,12 @@


class ResourcesBuilder:
def __init__(self, manager: 'HathorManager', event_ws_factory: Optional['EventWebsocketFactory']) -> None:
def __init__(
self,
manager: 'HathorManager',
event_ws_factory: Optional['EventWebsocketFactory'],
feature_service: FeatureService
) -> None:
self.log = logger.new()
self.manager = manager
self.event_ws_factory = event_ws_factory
Expand All @@ -42,6 +48,8 @@ def __init__(self, manager: 'HathorManager', event_ws_factory: Optional['EventWe
self._built_status = False
self._built_prometheus = False

self._feature_service = feature_service

def build(self, args: Namespace) -> Optional[server.Site]:
if args.prometheus:
self.create_prometheus(args)
Expand Down Expand Up @@ -76,6 +84,7 @@ def create_resources(self, args: Namespace) -> server.Site:
DebugRaiseResource,
DebugRejectResource,
)
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 +198,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(
feature_settings=settings.FEATURE_ACTIVATION,
feature_service=self._feature_service,
tx_storage=self.manager.tx_storage
),
root
)
]
# XXX: only enable UTXO search API if the index is enabled
if args.utxo_index:
Expand Down
12 changes: 8 additions & 4 deletions hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

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 @@ -147,15 +148,17 @@ def prepare(self, args: Namespace, *, register_resources: bool = True) -> None:
if args.stratum:
self.reactor.listenTCP(args.stratum, self.manager.stratum_factory)

from hathor.conf import HathorSettings
settings = HathorSettings()

feature_service = FeatureService(feature_settings=settings.FEATURE_ACTIVATION)

if register_resources:
resources_builder = ResourcesBuilder(self.manager, builder.event_ws_factory)
resources_builder = ResourcesBuilder(self.manager, builder.event_ws_factory, feature_service)
status_server = resources_builder.build(args)
if args.status:
self.reactor.listenTCP(args.status, status_server)

from hathor.conf import HathorSettings
settings = HathorSettings()

from hathor.builder.builder import BuildArtifacts
self.artifacts = BuildArtifacts(
peer_id=self.manager.my_peer,
Expand All @@ -171,6 +174,7 @@ def prepare(self, args: Namespace, *, register_resources: bool = True) -> None:
wallet=self.manager.wallet,
rocksdb_storage=getattr(builder, 'rocksdb_storage', None),
stratum_factory=self.manager.stratum_factory,
feature_service=feature_service
)

def start_sentry_if_possible(self, args: Namespace) -> None:
Expand Down
25 changes: 14 additions & 11 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
from hathor.feature_activation.model.criteria import Criteria
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
from hathor.feature_activation.settings import Settings as FeatureSettings
from hathor.transaction import Block


class FeatureService:
__slots__ = ('_settings',)
__slots__ = ('_feature_settings',)

def __init__(self, *, settings: Settings) -> None:
self._settings = settings
def __init__(self, *, feature_settings: FeatureSettings) -> None:
self._feature_settings = feature_settings

def is_feature_active(self, *, block: Block, feature: Feature) -> bool:
"""Returns whether a Feature is active at a certain block."""
Expand All @@ -42,8 +42,8 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
# 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.
height = block.get_height()
offset_to_boundary = height % self._settings.evaluation_interval
offset_to_previous_boundary = offset_to_boundary or self._settings.evaluation_interval
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 = _get_ancestor_at_height(block=block, height=previous_boundary_height)
previous_state = self.get_state(block=previous_boundary_block, feature=feature)
Expand All @@ -69,7 +69,9 @@ def _calculate_new_state(
criteria = self._get_criteria(feature=feature)

assert not boundary_block.is_genesis, 'cannot calculate new state for genesis'
assert height % self._settings.evaluation_interval == 0, 'cannot calculate new state for a non-boundary block'
assert height % self._feature_settings.evaluation_interval == 0, (
'cannot calculate new state for a non-boundary block'
)

if previous_state is FeatureState.DEFINED:
if height >= criteria.start_height:
Expand All @@ -90,9 +92,10 @@ 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
threshold = criteria.get_threshold(self._feature_settings)

if (
height < criteria.timeout_height
Expand All @@ -113,7 +116,7 @@ def _calculate_new_state(

def _get_criteria(self, *, feature: Feature) -> Criteria:
"""Get the Criteria defined for a specific Feature."""
criteria = self._settings.features.get(feature)
criteria = self._feature_settings.features.get(feature)

if not criteria:
raise ValueError(f"Criteria not defined for feature '{feature}'.")
Expand All @@ -127,7 +130,7 @@ def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescript
criteria=criteria,
state=self.get_state(block=block, feature=feature)
)
for feature, criteria in self._settings.features.items()
for feature, criteria in self._feature_settings.features.items()
}


Expand Down
9 changes: 8 additions & 1 deletion hathor/feature_activation/model/criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any, ClassVar, Optional
from typing import TYPE_CHECKING, Any, ClassVar, Optional

from pydantic import Field, NonNegativeInt, validator

from hathor import version
from hathor.utils.pydantic import BaseModel

if TYPE_CHECKING:
from hathor.feature_activation.settings import Settings as FeatureSettings


class Criteria(BaseModel, validate_all=True):
"""
Expand Down Expand Up @@ -55,6 +58,10 @@ class Criteria(BaseModel, validate_all=True):
activate_on_timeout: bool = False
version: str = Field(..., regex=version.BUILD_VERSION_REGEX)

def get_threshold(self, feature_settings: 'FeatureSettings') -> int:
"""Returns the configured threshold, or the default threshold if it is None."""
return self.threshold if self.threshold is not None else feature_settings.default_threshold

@validator('bit')
def _validate_bit(cls, bit: int) -> int:
"""Validates that the bit is lower than the max_signal_bits."""
Expand Down
Empty file.
154 changes: 154 additions & 0 deletions hathor/feature_activation/resources/feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# 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 twisted.web.http import Request

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 as FeatureSettings
from hathor.transaction.storage import TransactionStorage
from hathor.utils.api import Response


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

isLeaf = True

def __init__(
self,
*,
feature_settings: FeatureSettings,
feature_service: FeatureService,
tx_storage: TransactionStorage
) -> None:
super().__init__()
self._feature_settings = feature_settings
self._feature_service = feature_service
self.tx_storage = tx_storage

def render_GET(self, request: Request) -> bytes:
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._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
)

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):
block_hash: str
block_height: int
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': {
'block_hash': '00000000083580e5b299e9cb271fd5977103897e8640fcd5498767b6cefba6f5',
'block_height': 123,
'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'
}
]
}
}
}
}
}
}
}
}
}
Loading

0 comments on commit f3f3d19

Please sign in to comment.