diff --git a/hathor/builder/builder.py b/hathor/builder/builder.py index 7636aa2b3..d08550984 100644 --- a/hathor/builder/builder.py +++ b/hathor/builder/builder.py @@ -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] @@ -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 diff --git a/hathor/builder/sysctl_builder.py b/hathor/builder/sysctl_builder.py index e34cd4879..0b2131ad8 100644 --- a/hathor/builder/sysctl_builder.py +++ b/hathor/builder/sysctl_builder.py @@ -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: @@ -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 diff --git a/hathor/cli/run_node.py b/hathor/cli/run_node.py index 89f4e37de..6070de45f 100644 --- a/hathor/cli/run_node.py +++ b/hathor/cli/run_node.py @@ -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: diff --git a/hathor/feature_activation/bit_signaling_service.py b/hathor/feature_activation/bit_signaling_service.py index 6585086b4..b3da0d394 100644 --- a/hathor/feature_activation/bit_signaling_service.py +++ b/hathor/feature_activation/bit_signaling_service.py @@ -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.""" @@ -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): diff --git a/hathor/sysctl/__init__.py b/hathor/sysctl/__init__.py index af9d30e17..a73637650 100644 --- a/hathor/sysctl/__init__.py +++ b/hathor/sysctl/__init__.py @@ -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 @@ -22,4 +23,5 @@ 'ConnectionsManagerSysctl', 'HathorManagerSysctl', 'WebsocketManagerSysctl', + 'FeatureActivationSysctl', ] diff --git a/hathor/sysctl/feature_activation/__init__.py b/hathor/sysctl/feature_activation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/hathor/sysctl/feature_activation/manager.py b/hathor/sysctl/feature_activation/manager.py new file mode 100644 index 000000000..2649d26b8 --- /dev/null +++ b/hathor/sysctl/feature_activation/manager.py @@ -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] diff --git a/tests/sysctl/test_feature_activation.py b/tests/sysctl/test_feature_activation.py new file mode 100644 index 000000000..48aeb2713 --- /dev/null +++ b/tests/sysctl/test_feature_activation.py @@ -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)