Skip to content

Commit

Permalink
feat(feature-activation): implement bit signaling service (#702)
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco authored Jul 19, 2023
1 parent 7bee591 commit c770612
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 0 deletions.
6 changes: 6 additions & 0 deletions hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from hathor.cli.run_node_args import RunNodeArgs
from hathor.conf import TESTNET_SETTINGS_FILEPATH, HathorSettings
from hathor.exception import PreInitializationError
from hathor.feature_activation.feature import Feature

logger = get_logger()
# LOGGING_CAPTURE_STDOUT = True
Expand Down Expand Up @@ -112,6 +113,11 @@ def create_parser(cls) -> ArgumentParser:
parser.add_argument('--peer-id-blacklist', action='extend', default=[], nargs='+', type=str,
help='Peer IDs to forbid connection')
parser.add_argument('--config-yaml', type=str, help='Configuration yaml filepath')
possible_features = [feature.value for feature in Feature]
parser.add_argument('--signal-support', default=[], action='append', choices=possible_features,
help=f'Signal support for a feature. One of {possible_features}')
parser.add_argument('--signal-not-support', default=[], action='append', choices=possible_features,
help=f'Signal not support for a feature. One of {possible_features}')
return parser

def prepare(self, *, register_resources: bool = True) -> None:
Expand Down
3 changes: 3 additions & 0 deletions hathor/cli/run_node_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from pydantic import Extra

from hathor.feature_activation.feature import Feature
from hathor.utils.pydantic import BaseModel


Expand Down Expand Up @@ -70,3 +71,5 @@ class RunNodeArgs(BaseModel, extra=Extra.allow):
x_enable_event_queue: bool
peer_id_blacklist: list[str]
config_yaml: Optional[str]
signal_support: set[Feature]
signal_not_support: set[Feature]
147 changes: 147 additions & 0 deletions hathor/feature_activation/bit_signaling_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# 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 structlog import get_logger

from hathor.feature_activation.feature import Feature
from hathor.feature_activation.feature_service import FeatureService
from hathor.feature_activation.model.criteria import Criteria
from hathor.feature_activation.model.feature_state import FeatureState
from hathor.feature_activation.settings import Settings as FeatureSettings
from hathor.transaction import Block
from hathor.transaction.storage import TransactionStorage

logger = get_logger()


class BitSignalingService:
__slots__ = (
'_log',
'_feature_settings',
'_feature_service',
'_tx_storage',
'_support_features',
'_not_support_features'
)

def __init__(
self,
*,
feature_settings: FeatureSettings,
feature_service: FeatureService,
tx_storage: TransactionStorage,
support_features: set[Feature],
not_support_features: set[Feature]
) -> None:
self._log = logger.new()
self._feature_settings = feature_settings
self._feature_service = feature_service
self._tx_storage = tx_storage
self._support_features = support_features
self._not_support_features = not_support_features

self._validate_support_intersection()

def start(self) -> None:
best_block = self._tx_storage.get_best_block()

self._warn_non_signaling_features(best_block)
self._log_feature_signals(best_block)

def generate_signal_bits(self, *, block: Block, log: bool = False) -> int:
"""
Generate signal bits considering a given block. The block is used to determine which features are currently in
a signaling period.
Args:
block: the block that is used to determine signaling features.
log: whether to log the signal for each feature.
Returns: a number that represents the signal bits in binary.
"""
signaling_features = self._get_signaling_features(block)
signal_bits = 0

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

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

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

return signal_bits

def _log_signal_bits(self, feature: Feature, enable_bit: bool, support: bool, not_support: bool) -> None:
"""Generate info log for a feature's signal."""
signal = 'enabled' if enable_bit else 'disabled'
reason = 'using default feature signal'

if support:
reason = 'user signaled support'

if not_support:
reason = 'user signaled not support'

self._log.info(
'Configuring support signal for feature.',
feature=feature.value,
signal=signal,
reason=reason
)

def _get_signaling_features(self, block: Block) -> dict[Feature, Criteria]:
"""Given a specific block, return all features that are in a signaling state for that block."""
feature_descriptions = self._feature_service.get_bits_description(block=block)
signaling_features = {
feature: description.criteria
for feature, description in feature_descriptions.items()
if description.state in FeatureState.get_signaling_states()
}

assert len(signaling_features) <= self._feature_settings.max_signal_bits, (
'Invalid state. Signaling more features than the allowed maximum.'
)

return signaling_features

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):
feature_names = [feature.value for feature in intersection]
raise ValueError(f'Cannot signal both "support" and "not support" for features {feature_names}')

def _warn_non_signaling_features(self, best_block: Block) -> None:
"""Generate a warning log if any signaled features are currently not in a signaling state."""
currently_signaling_features = self._get_signaling_features(best_block)
signaled_features = self._support_features.union(self._not_support_features)

if non_signaling_features := signaled_features.difference(currently_signaling_features):
feature_names = {feature.value for feature in non_signaling_features}
self._log.warn(
'Considering the current best block, there are signaled features outside their signaling period. '
'Therefore, signaling for them has no effect. Make sure you are signaling for the desired features.',
best_block_hash=best_block.hash_hex,
best_block_height=best_block.get_height(),
non_signaling_features=feature_names
)

def _log_feature_signals(self, best_block: Block) -> None:
"""Generate info logs for each feature's current signal."""
signal_bits = self.generate_signal_bits(block=best_block, log=True)

self._log.debug(f'Configured signal bits: {bin(signal_bits)[2:]}')
1 change: 1 addition & 0 deletions hathor/feature_activation/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ class Feature(Enum):

NOP_FEATURE_1 = 'NOP_FEATURE_1'
NOP_FEATURE_2 = 'NOP_FEATURE_2'
NOP_FEATURE_3 = 'NOP_FEATURE_3'
4 changes: 4 additions & 0 deletions hathor/feature_activation/model/criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class Criteria(BaseModel, validate_all=True):
the timeout_height is reached, effectively forcing activation.
version: the client version of hathor-core at which this feature was defined.
signal_support_by_default: the default miner support signal for this feature.
"""
evaluation_interval: Optional[int] = None
max_signal_bits: Optional[int] = None
Expand All @@ -59,6 +61,7 @@ class Criteria(BaseModel, validate_all=True):
minimum_activation_height: NonNegativeInt = 0
lock_in_on_timeout: bool = False
version: str = Field(..., regex=version.BUILD_VERSION_REGEX)
signal_support_by_default: bool = False

def to_validated(self, evaluation_interval: int, max_signal_bits: int) -> 'ValidatedCriteria':
"""Create a validated version of self, including attribute validations that have external dependencies."""
Expand All @@ -72,6 +75,7 @@ def to_validated(self, evaluation_interval: int, max_signal_bits: int) -> 'Valid
minimum_activation_height=self.minimum_activation_height,
lock_in_on_timeout=self.lock_in_on_timeout,
version=self.version,
signal_support_by_default=self.signal_support_by_default
)

def get_threshold(self, feature_settings: 'FeatureSettings') -> int:
Expand Down
10 changes: 10 additions & 0 deletions hathor/feature_activation/model/feature_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class FeatureState(Enum):
Attributes:
DEFINED: Represents that a feature is defined. It's the first state for each feature.
STARTED: Represents that the activation process for some feature is started.
MUST_SIGNAL: Represents that a feature is going to be locked-in, and that miners must signal support for it.
LOCKED_IN: Represents that a feature is going to be activated.
ACTIVE: Represents that a certain feature is activated.
FAILED: Represents that a certain feature is not and will never be activated.
"""
Expand All @@ -32,3 +34,11 @@ class FeatureState(Enum):
LOCKED_IN = 'LOCKED_IN'
ACTIVE = 'ACTIVE'
FAILED = 'FAILED'

@staticmethod
def get_signaling_states() -> set['FeatureState']:
"""
Return the states for which a feature is considered in its signaling period, that is, voting to either
support it or not through bit signals is valid during those states.
"""
return {FeatureState.STARTED, FeatureState.MUST_SIGNAL, FeatureState.LOCKED_IN}
Loading

0 comments on commit c770612

Please sign in to comment.