Skip to content

Commit

Permalink
feat(feature-activation): implement bit signaling sysctl
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Apr 2, 2024
1 parent 881c5ff commit 4b44a2a
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 6 deletions.
2 changes: 2 additions & 0 deletions hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class BuildArtifacts(NamedTuple):
consensus: ConsensusAlgorithm
tx_storage: TransactionStorage
feature_service: FeatureService
bit_signaling_service: BitSignalingService
indexes: Optional[IndexesManager]
wallet: Optional[BaseWallet]
rocksdb_storage: Optional[RocksDBStorage]
Expand Down Expand Up @@ -247,6 +248,7 @@ def build(self) -> BuildArtifacts:
rocksdb_storage=self._rocksdb_storage,
stratum_factory=stratum_factory,
feature_service=feature_service,
bit_signaling_service=bit_signaling_service
)

return self.artifacts
Expand Down
14 changes: 12 additions & 2 deletions hathor/builder/sysctl_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
# limitations under the License.

from hathor.builder import BuildArtifacts
from hathor.sysctl import ConnectionsManagerSysctl, HathorManagerSysctl, Sysctl, WebsocketManagerSysctl
from hathor.sysctl import (
ConnectionsManagerSysctl,
FeatureActivationSysctl,
HathorManagerSysctl,
Sysctl,
WebsocketManagerSysctl,
)


class SysctlBuilder:
Expand All @@ -25,7 +31,11 @@ def __init__(self, artifacts: BuildArtifacts) -> None:
def build(self) -> Sysctl:
"""Build the sysctl tree."""
root = Sysctl()
root.put_child('core', HathorManagerSysctl(self.artifacts.manager))

core = HathorManagerSysctl(self.artifacts.manager)
core.put_child('features', FeatureActivationSysctl(self.artifacts.bit_signaling_service))

root.put_child('core', core)
root.put_child('p2p', ConnectionsManagerSysctl(self.artifacts.p2p_manager))

ws_factory = self.artifacts.manager.metrics.websocket_factory
Expand Down
3 changes: 2 additions & 1 deletion hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ def prepare(self, *, register_resources: bool = True) -> None:
wallet=self.manager.wallet,
rocksdb_storage=getattr(builder, 'rocksdb_storage', None),
stratum_factory=self.manager.stratum_factory,
feature_service=self.manager._feature_service
feature_service=self.manager._feature_service,
bit_signaling_service=self.manager._bit_signaling_service,
)

def start_sentry_if_possible(self) -> None:
Expand Down
50 changes: 47 additions & 3 deletions hathor/feature_activation/bit_signaling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,60 @@ def generate_signal_bits(self, *, block: Block, log: bool = False) -> int:
Returns: a number that represents the signal bits in binary.
"""
signaling_features = self._get_signaling_features(block)
feature_signals = self._calculate_feature_signals(block=block, log=log)
signal_bits = 0

for feature, (criteria, enable_bit) in feature_signals.items():
signal_bits |= int(enable_bit) << criteria.bit

return signal_bits

def _calculate_feature_signals(self, *, block: Block, log: bool = False) -> dict[Feature, tuple[Criteria, bool]]:
"""
Calculate the signal value for each signaling feature.
Args:
block: the block that is used to determine signaling features.
log: whether to log the signal for each feature.
Returns: a dict with each feature paired with its criteria and its signal value.
"""
signaling_features = self._get_signaling_features(block)
signals: dict[Feature, tuple[Criteria, bool]] = {}

for feature, criteria in signaling_features.items():
default_enable_bit = criteria.signal_support_by_default
support = feature in self._support_features
not_support = feature in self._not_support_features
enable_bit = (default_enable_bit or support) and not not_support
signals[feature] = (criteria, enable_bit)

if log:
self._log_signal_bits(feature, enable_bit, support, not_support)

signal_bits |= int(enable_bit) << criteria.bit
return signals

return signal_bits
def get_support_features(self) -> list[Feature]:
"""Get a list of features with enabled support."""
best_block = self._tx_storage.get_best_block()
feature_signals = self._calculate_feature_signals(block=best_block)
return [feature for feature, (_, enable_bit) in feature_signals.items() if enable_bit]

def get_not_support_features(self) -> list[Feature]:
"""Get a list of features with disabled support."""
best_block = self._tx_storage.get_best_block()
feature_signals = self._calculate_feature_signals(block=best_block)
return [feature for feature, (_, enable_bit) in feature_signals.items() if not enable_bit]

def add_feature_support(self, feature: Feature) -> None:
"""Add explicit support for a feature by enabling its signaling bit."""
self._not_support_features.discard(feature)
self._support_features.add(feature)

def remove_feature_support(self, feature: Feature) -> None:
"""Remove explicit support for a feature by disabling its signaling bit."""
self._support_features.discard(feature)
self._not_support_features.add(feature)

def _log_signal_bits(self, feature: Feature, enable_bit: bool, support: bool, not_support: bool) -> None:
"""Generate info log for a feature's signal."""
Expand Down Expand Up @@ -130,6 +169,11 @@ def _get_signaling_features(self, block: Block) -> dict[Feature, Criteria]:

return signaling_features

def get_best_block_signaling_features(self) -> dict[Feature, Criteria]:
"""Given the current best block, return all features that are in a signaling state."""
best_block = self._tx_storage.get_best_block()
return self._get_signaling_features(best_block)

def _validate_support_intersection(self) -> None:
"""Validate that the provided support and not-support arguments do not conflict."""
if intersection := self._support_features.intersection(self._not_support_features):
Expand Down
2 changes: 2 additions & 0 deletions hathor/sysctl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

from hathor.sysctl.core.manager import HathorManagerSysctl
from hathor.sysctl.feature_activation.manager import FeatureActivationSysctl
from hathor.sysctl.p2p.manager import ConnectionsManagerSysctl
from hathor.sysctl.sysctl import Sysctl
from hathor.sysctl.websocket.manager import WebsocketManagerSysctl
Expand All @@ -22,4 +23,5 @@
'ConnectionsManagerSysctl',
'HathorManagerSysctl',
'WebsocketManagerSysctl',
'FeatureActivationSysctl',
]
Empty file.
72 changes: 72 additions & 0 deletions hathor/sysctl/feature_activation/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2024 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 hathor.feature_activation.bit_signaling_service import BitSignalingService
from hathor.feature_activation.feature import Feature
from hathor.sysctl.sysctl import Sysctl


class FeatureActivationSysctl(Sysctl):
def __init__(self, bit_signaling_service: BitSignalingService) -> None:
super().__init__()
self._bit_signaling_service = bit_signaling_service

self.register(
path='supported_features',
getter=self.get_support_features,
setter=None,
)
self.register(
path='not_supported_features',
getter=self.get_not_support_features,
setter=None,
)
self.register(
path='signaling_features',
getter=self.get_signaling_features,
setter=None,
)
self.register(
path='add_support',
getter=None,
setter=self.add_feature_support,
)
self.register(
path='remove_support',
getter=None,
setter=self.remove_feature_support,
)

def get_support_features(self) -> list[str]:
"""Get a list of feature names with enabled support."""
return [feature.value for feature in self._bit_signaling_service.get_support_features()]

def get_not_support_features(self) -> list[str]:
"""Get a list of feature names with disabled support."""
return [feature.value for feature in self._bit_signaling_service.get_not_support_features()]

def add_feature_support(self, *features: str) -> None:
"""Explicitly add support for a feature by enabling its signaling bit."""
for feature in features:
self._bit_signaling_service.add_feature_support(Feature[feature])

def remove_feature_support(self, *features: str) -> None:
"""Explicitly remove support for a feature by disabling its signaling bit."""
for feature in features:
self._bit_signaling_service.remove_feature_support(Feature[feature])

def get_signaling_features(self) -> list[str]:
"""Get a list of feature names that are currently in a signaling state."""
features = self._bit_signaling_service.get_best_block_signaling_features().keys()
return [feature.value for feature in features]
38 changes: 38 additions & 0 deletions tests/sysctl/test_feature_activation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2024 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

from hathor.feature_activation.bit_signaling_service import BitSignalingService
from hathor.feature_activation.feature import Feature
from hathor.sysctl import FeatureActivationSysctl


def test_feature_activation_sysctl() -> None:
bit_signaling_service_mock = Mock(spec_set=BitSignalingService)
sysctl = FeatureActivationSysctl(bit_signaling_service_mock)

bit_signaling_service_mock.get_support_features = Mock(return_value=[Feature.NOP_FEATURE_1, Feature.NOP_FEATURE_2])
bit_signaling_service_mock.get_not_support_features = Mock(return_value=[Feature.NOP_FEATURE_3])
bit_signaling_service_mock.get_best_block_signaling_features = Mock(return_value={Feature.NOP_FEATURE_1: Mock()})

assert sysctl.get('supported_features') == ['NOP_FEATURE_1', 'NOP_FEATURE_2']
assert sysctl.get('not_supported_features') == ['NOP_FEATURE_3']
assert sysctl.get('signaling_features') == ['NOP_FEATURE_1']

sysctl.unsafe_set('add_support', 'NOP_FEATURE_3')
bit_signaling_service_mock.add_feature_support.assert_called_once_with(Feature.NOP_FEATURE_3)

sysctl.unsafe_set('remove_support', 'NOP_FEATURE_1')
bit_signaling_service_mock.remove_feature_support.assert_called_once_with(Feature.NOP_FEATURE_1)

0 comments on commit 4b44a2a

Please sign in to comment.