Skip to content

Commit 3ff748d

Browse files
authored
feat(feature-activation): implement bit signaling sysctl (#962)
1 parent 881c5ff commit 3ff748d

File tree

8 files changed

+175
-6
lines changed

8 files changed

+175
-6
lines changed

hathor/builder/builder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class BuildArtifacts(NamedTuple):
7171
consensus: ConsensusAlgorithm
7272
tx_storage: TransactionStorage
7373
feature_service: FeatureService
74+
bit_signaling_service: BitSignalingService
7475
indexes: Optional[IndexesManager]
7576
wallet: Optional[BaseWallet]
7677
rocksdb_storage: Optional[RocksDBStorage]
@@ -247,6 +248,7 @@ def build(self) -> BuildArtifacts:
247248
rocksdb_storage=self._rocksdb_storage,
248249
stratum_factory=stratum_factory,
249250
feature_service=feature_service,
251+
bit_signaling_service=bit_signaling_service
250252
)
251253

252254
return self.artifacts

hathor/builder/sysctl_builder.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@
1313
# limitations under the License.
1414

1515
from hathor.builder import BuildArtifacts
16-
from hathor.sysctl import ConnectionsManagerSysctl, HathorManagerSysctl, Sysctl, WebsocketManagerSysctl
16+
from hathor.sysctl import (
17+
ConnectionsManagerSysctl,
18+
FeatureActivationSysctl,
19+
HathorManagerSysctl,
20+
Sysctl,
21+
WebsocketManagerSysctl,
22+
)
1723

1824

1925
class SysctlBuilder:
@@ -25,7 +31,11 @@ def __init__(self, artifacts: BuildArtifacts) -> None:
2531
def build(self) -> Sysctl:
2632
"""Build the sysctl tree."""
2733
root = Sysctl()
28-
root.put_child('core', HathorManagerSysctl(self.artifacts.manager))
34+
35+
core = HathorManagerSysctl(self.artifacts.manager)
36+
core.put_child('features', FeatureActivationSysctl(self.artifacts.bit_signaling_service))
37+
38+
root.put_child('core', core)
2939
root.put_child('p2p', ConnectionsManagerSysctl(self.artifacts.p2p_manager))
3040

3141
ws_factory = self.artifacts.manager.metrics.websocket_factory

hathor/cli/run_node.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ def prepare(self, *, register_resources: bool = True) -> None:
221221
wallet=self.manager.wallet,
222222
rocksdb_storage=getattr(builder, 'rocksdb_storage', None),
223223
stratum_factory=self.manager.stratum_factory,
224-
feature_service=self.manager._feature_service
224+
feature_service=self.manager._feature_service,
225+
bit_signaling_service=self.manager._bit_signaling_service,
225226
)
226227

227228
def start_sentry_if_possible(self) -> None:

hathor/feature_activation/bit_signaling_service.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,60 @@ def generate_signal_bits(self, *, block: Block, log: bool = False) -> int:
8181
8282
Returns: a number that represents the signal bits in binary.
8383
"""
84-
signaling_features = self._get_signaling_features(block)
84+
feature_signals = self._calculate_feature_signals(block=block, log=log)
8585
signal_bits = 0
8686

87+
for feature, (criteria, enable_bit) in feature_signals.items():
88+
signal_bits |= int(enable_bit) << criteria.bit
89+
90+
return signal_bits
91+
92+
def _calculate_feature_signals(self, *, block: Block, log: bool = False) -> dict[Feature, tuple[Criteria, bool]]:
93+
"""
94+
Calculate the signal value for each signaling feature.
95+
96+
Args:
97+
block: the block that is used to determine signaling features.
98+
log: whether to log the signal for each feature.
99+
100+
Returns: a dict with each feature paired with its criteria and its signal value.
101+
"""
102+
signaling_features = self._get_signaling_features(block)
103+
signals: dict[Feature, tuple[Criteria, bool]] = {}
104+
87105
for feature, criteria in signaling_features.items():
88106
default_enable_bit = criteria.signal_support_by_default
89107
support = feature in self._support_features
90108
not_support = feature in self._not_support_features
91109
enable_bit = (default_enable_bit or support) and not not_support
110+
signals[feature] = (criteria, enable_bit)
92111

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

96-
signal_bits |= int(enable_bit) << criteria.bit
115+
return signals
97116

98-
return signal_bits
117+
def get_support_features(self) -> list[Feature]:
118+
"""Get a list of features with enabled support."""
119+
best_block = self._tx_storage.get_best_block()
120+
feature_signals = self._calculate_feature_signals(block=best_block)
121+
return [feature for feature, (_, enable_bit) in feature_signals.items() if enable_bit]
122+
123+
def get_not_support_features(self) -> list[Feature]:
124+
"""Get a list of features with disabled support."""
125+
best_block = self._tx_storage.get_best_block()
126+
feature_signals = self._calculate_feature_signals(block=best_block)
127+
return [feature for feature, (_, enable_bit) in feature_signals.items() if not enable_bit]
128+
129+
def add_feature_support(self, feature: Feature) -> None:
130+
"""Add explicit support for a feature by enabling its signaling bit."""
131+
self._not_support_features.discard(feature)
132+
self._support_features.add(feature)
133+
134+
def remove_feature_support(self, feature: Feature) -> None:
135+
"""Remove explicit support for a feature by disabling its signaling bit."""
136+
self._support_features.discard(feature)
137+
self._not_support_features.add(feature)
99138

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

131170
return signaling_features
132171

172+
def get_best_block_signaling_features(self) -> dict[Feature, Criteria]:
173+
"""Given the current best block, return all features that are in a signaling state."""
174+
best_block = self._tx_storage.get_best_block()
175+
return self._get_signaling_features(best_block)
176+
133177
def _validate_support_intersection(self) -> None:
134178
"""Validate that the provided support and not-support arguments do not conflict."""
135179
if intersection := self._support_features.intersection(self._not_support_features):

hathor/sysctl/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from hathor.sysctl.core.manager import HathorManagerSysctl
16+
from hathor.sysctl.feature_activation.manager import FeatureActivationSysctl
1617
from hathor.sysctl.p2p.manager import ConnectionsManagerSysctl
1718
from hathor.sysctl.sysctl import Sysctl
1819
from hathor.sysctl.websocket.manager import WebsocketManagerSysctl
@@ -22,4 +23,5 @@
2223
'ConnectionsManagerSysctl',
2324
'HathorManagerSysctl',
2425
'WebsocketManagerSysctl',
26+
'FeatureActivationSysctl',
2527
]

hathor/sysctl/feature_activation/__init__.py

Whitespace-only changes.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from hathor.feature_activation.bit_signaling_service import BitSignalingService
16+
from hathor.feature_activation.feature import Feature
17+
from hathor.sysctl.sysctl import Sysctl
18+
19+
20+
class FeatureActivationSysctl(Sysctl):
21+
def __init__(self, bit_signaling_service: BitSignalingService) -> None:
22+
super().__init__()
23+
self._bit_signaling_service = bit_signaling_service
24+
25+
self.register(
26+
path='supported_features',
27+
getter=self.get_support_features,
28+
setter=None,
29+
)
30+
self.register(
31+
path='not_supported_features',
32+
getter=self.get_not_support_features,
33+
setter=None,
34+
)
35+
self.register(
36+
path='signaling_features',
37+
getter=self.get_signaling_features,
38+
setter=None,
39+
)
40+
self.register(
41+
path='add_support',
42+
getter=None,
43+
setter=self.add_feature_support,
44+
)
45+
self.register(
46+
path='remove_support',
47+
getter=None,
48+
setter=self.remove_feature_support,
49+
)
50+
51+
def get_support_features(self) -> list[str]:
52+
"""Get a list of feature names with enabled support."""
53+
return [feature.value for feature in self._bit_signaling_service.get_support_features()]
54+
55+
def get_not_support_features(self) -> list[str]:
56+
"""Get a list of feature names with disabled support."""
57+
return [feature.value for feature in self._bit_signaling_service.get_not_support_features()]
58+
59+
def add_feature_support(self, *features: str) -> None:
60+
"""Explicitly add support for a feature by enabling its signaling bit."""
61+
for feature in features:
62+
self._bit_signaling_service.add_feature_support(Feature[feature])
63+
64+
def remove_feature_support(self, *features: str) -> None:
65+
"""Explicitly remove support for a feature by disabling its signaling bit."""
66+
for feature in features:
67+
self._bit_signaling_service.remove_feature_support(Feature[feature])
68+
69+
def get_signaling_features(self) -> list[str]:
70+
"""Get a list of feature names that are currently in a signaling state."""
71+
features = self._bit_signaling_service.get_best_block_signaling_features().keys()
72+
return [feature.value for feature in features]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2024 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest.mock import Mock
16+
17+
from hathor.feature_activation.bit_signaling_service import BitSignalingService
18+
from hathor.feature_activation.feature import Feature
19+
from hathor.sysctl import FeatureActivationSysctl
20+
21+
22+
def test_feature_activation_sysctl() -> None:
23+
bit_signaling_service_mock = Mock(spec_set=BitSignalingService)
24+
sysctl = FeatureActivationSysctl(bit_signaling_service_mock)
25+
26+
bit_signaling_service_mock.get_support_features = Mock(return_value=[Feature.NOP_FEATURE_1, Feature.NOP_FEATURE_2])
27+
bit_signaling_service_mock.get_not_support_features = Mock(return_value=[Feature.NOP_FEATURE_3])
28+
bit_signaling_service_mock.get_best_block_signaling_features = Mock(return_value={Feature.NOP_FEATURE_1: Mock()})
29+
30+
assert sysctl.get('supported_features') == ['NOP_FEATURE_1', 'NOP_FEATURE_2']
31+
assert sysctl.get('not_supported_features') == ['NOP_FEATURE_3']
32+
assert sysctl.get('signaling_features') == ['NOP_FEATURE_1']
33+
34+
sysctl.unsafe_set('add_support', 'NOP_FEATURE_3')
35+
bit_signaling_service_mock.add_feature_support.assert_called_once_with(Feature.NOP_FEATURE_3)
36+
37+
sysctl.unsafe_set('remove_support', 'NOP_FEATURE_1')
38+
bit_signaling_service_mock.remove_feature_support.assert_called_once_with(Feature.NOP_FEATURE_1)

0 commit comments

Comments
 (0)