Skip to content
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 get endpoint #634

Merged
merged 1 commit into from
Jun 14, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions hathor/builder/builder.py
Original file line number Diff line number Diff line change
@@ -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
@@ -63,6 +64,7 @@ class BuildArtifacts(NamedTuple):
wallet: Optional[BaseWallet]
rocksdb_storage: Optional[RocksDBStorage]
stratum_factory: Optional[StratumFactory]
feature_service: FeatureService


class Builder:
@@ -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,
@@ -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
@@ -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

21 changes: 20 additions & 1 deletion hathor/builder/resources_builder.py
Original file line number Diff line number Diff line change
@@ -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:
@@ -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
@@ -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)
@@ -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,
@@ -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:
12 changes: 8 additions & 4 deletions hathor/cli/run_node.py
Original file line number Diff line number Diff line change
@@ -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
@@ -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,
@@ -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:
25 changes: 14 additions & 11 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
@@ -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."""
@@ -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)
@@ -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:
@@ -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
@@ -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}'.")
@@ -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()
}


9 changes: 8 additions & 1 deletion hathor/feature_activation/model/criteria.py
Original file line number Diff line number Diff line change
@@ -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):
"""
@@ -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:
msbrogli marked this conversation as resolved.
Show resolved Hide resolved
"""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."""
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()
msbrogli marked this conversation as resolved.
Show resolved Hide resolved
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