Skip to content

feat(feature-activation): implement bit signaling sysctl #962

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

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
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
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)