Skip to content

feat(feature-activation): implement must signal #785

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
Nov 1, 2023
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
44 changes: 35 additions & 9 deletions hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

from enum import Enum
from typing import Any, NamedTuple, Optional
from typing import Any, Callable, NamedTuple, Optional

from structlog import get_logger

Expand Down Expand Up @@ -63,6 +63,7 @@ class BuildArtifacts(NamedTuple):
pubsub: PubSubManager
consensus: ConsensusAlgorithm
tx_storage: TransactionStorage
feature_service: FeatureService
indexes: Optional[IndexesManager]
wallet: Optional[BaseWallet]
rocksdb_storage: Optional[RocksDBStorage]
Expand Down Expand Up @@ -103,6 +104,7 @@ def __init__(self) -> None:
self._bit_signaling_service: Optional[BitSignalingService] = None

self._vertex_verifiers: Optional[VertexVerifiers] = None
self._vertex_verifiers_builder: Callable[[HathorSettingsType, FeatureService], VertexVerifiers] | None = None
self._verification_service: Optional[VerificationService] = None

self._rocksdb_path: Optional[str] = None
Expand Down Expand Up @@ -158,9 +160,9 @@ def build(self) -> BuildArtifacts:
wallet = self._get_or_create_wallet()
event_manager = self._get_or_create_event_manager()
indexes = self._get_or_create_indexes_manager()
tx_storage = self._get_or_create_tx_storage(indexes)
feature_service = self._get_or_create_feature_service(tx_storage)
bit_signaling_service = self._get_or_create_bit_signaling_service(tx_storage)
tx_storage = self._get_or_create_tx_storage()
feature_service = self._get_or_create_feature_service()
bit_signaling_service = self._get_or_create_bit_signaling_service()
verification_service = self._get_or_create_verification_service()

if self._enable_address_index:
Expand Down Expand Up @@ -221,6 +223,7 @@ def build(self) -> BuildArtifacts:
wallet=wallet,
rocksdb_storage=self._rocksdb_storage,
stratum_factory=stratum_factory,
feature_service=feature_service,
)

return self.artifacts
Expand Down Expand Up @@ -265,6 +268,7 @@ def set_peer_id(self, peer_id: PeerId) -> 'Builder':
return self

def _get_or_create_settings(self) -> HathorSettingsType:
"""Return the HathorSettings instance set on this builder, or a new one if not set."""
if self._settings is None:
self._settings = get_settings()
return self._settings
Expand Down Expand Up @@ -352,7 +356,9 @@ def _get_or_create_indexes_manager(self) -> IndexesManager:

return self._indexes_manager

def _get_or_create_tx_storage(self, indexes: IndexesManager) -> TransactionStorage:
def _get_or_create_tx_storage(self) -> TransactionStorage:
indexes = self._get_or_create_indexes_manager()

if self._tx_storage is not None:
# If a tx storage is provided, set the indexes manager to it.
self._tx_storage.indexes = indexes
Expand Down Expand Up @@ -415,22 +421,26 @@ def _get_or_create_event_manager(self) -> EventManager:

return self._event_manager

def _get_or_create_feature_service(self, tx_storage: TransactionStorage) -> FeatureService:
def _get_or_create_feature_service(self) -> FeatureService:
"""Return the FeatureService instance set on this builder, or a new one if not set."""
if self._feature_service is None:
settings = self._get_or_create_settings()
tx_storage = self._get_or_create_tx_storage()
self._feature_service = FeatureService(
feature_settings=settings.FEATURE_ACTIVATION,
tx_storage=tx_storage
)

return self._feature_service

def _get_or_create_bit_signaling_service(self, tx_storage: TransactionStorage) -> BitSignalingService:
def _get_or_create_bit_signaling_service(self) -> BitSignalingService:
if self._bit_signaling_service is None:
settings = self._get_or_create_settings()
tx_storage = self._get_or_create_tx_storage()
feature_service = self._get_or_create_feature_service()
self._bit_signaling_service = BitSignalingService(
feature_settings=settings.FEATURE_ACTIVATION,
feature_service=self._get_or_create_feature_service(tx_storage),
feature_service=feature_service,
tx_storage=tx_storage,
support_features=self._support_features,
not_support_features=self._not_support_features,
Expand All @@ -448,7 +458,15 @@ def _get_or_create_verification_service(self) -> VerificationService:
def _get_or_create_vertex_verifiers(self) -> VertexVerifiers:
if self._vertex_verifiers is None:
settings = self._get_or_create_settings()
self._vertex_verifiers = VertexVerifiers.create_defaults(settings=settings)
feature_service = self._get_or_create_feature_service()

if self._vertex_verifiers_builder:
self._vertex_verifiers = self._vertex_verifiers_builder(settings, feature_service)
else:
self._vertex_verifiers = VertexVerifiers.create_defaults(
settings=settings,
feature_service=feature_service
)

return self._vertex_verifiers

Expand Down Expand Up @@ -554,6 +572,14 @@ def set_vertex_verifiers(self, vertex_verifiers: VertexVerifiers) -> 'Builder':
self._vertex_verifiers = vertex_verifiers
return self

def set_vertex_verifiers_builder(
self,
builder: Callable[[HathorSettingsType, FeatureService], VertexVerifiers]
) -> 'Builder':
self.check_if_can_modify()
self._vertex_verifiers_builder = builder
return self

def set_reactor(self, reactor: Reactor) -> 'Builder':
self.check_if_can_modify()
self._reactor = reactor
Expand Down
2 changes: 1 addition & 1 deletion hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
not_support_features=self._args.signal_not_support
)

vertex_verifiers = VertexVerifiers.create_defaults(settings=settings)
vertex_verifiers = VertexVerifiers.create_defaults(settings=settings, feature_service=self.feature_service)
verification_service = VerificationService(verifiers=vertex_verifiers)

p2p_manager = ConnectionsManager(
Expand Down
1 change: 1 addition & 0 deletions hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ 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
)

def start_sentry_if_possible(self) -> None:
Expand Down
73 changes: 63 additions & 10 deletions hathor/feature_activation/feature_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,75 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeAlias

from hathor.feature_activation.feature import Feature
from hathor.feature_activation.model.feature_description import FeatureDescription
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

if TYPE_CHECKING:
from hathor.transaction import Block
from hathor.transaction.storage import TransactionStorage


@dataclass(frozen=True, slots=True)
class BlockIsSignaling:
"""Represent that a block is correctly signaling support for all currently mandatory features."""
pass


@dataclass(frozen=True, slots=True)
class BlockIsMissingSignal:
"""Represent that a block is not signaling support for at least one currently mandatory feature."""
feature: Feature


BlockSignalingState: TypeAlias = BlockIsSignaling | BlockIsMissingSignal


class FeatureService:
__slots__ = ('_feature_settings', '_tx_storage')

def __init__(self, *, feature_settings: FeatureSettings, tx_storage: TransactionStorage) -> None:
def __init__(self, *, feature_settings: FeatureSettings, tx_storage: 'TransactionStorage') -> None:
self._feature_settings = feature_settings
self._tx_storage = tx_storage

def is_feature_active(self, *, block: Block, feature: Feature) -> bool:
def is_feature_active(self, *, block: 'Block', feature: Feature) -> bool:
"""Returns whether a Feature is active at a certain block."""
state = self.get_state(block=block, feature=feature)

return state == FeatureState.ACTIVE

def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
def is_signaling_mandatory_features(self, block: 'Block') -> BlockSignalingState:
"""
Return whether a block is signaling features that are mandatory, that is, any feature currently in the
MUST_SIGNAL phase.
"""
bit_counts = block.get_feature_activation_bit_counts()
height = block.get_height()
offset_to_boundary = height % self._feature_settings.evaluation_interval
remaining_blocks = self._feature_settings.evaluation_interval - offset_to_boundary - 1
descriptions = self.get_bits_description(block=block)

must_signal_features = (
feature for feature, description in descriptions.items()
if description.state is FeatureState.MUST_SIGNAL
)

for feature in must_signal_features:
criteria = self._feature_settings.features[feature]
threshold = criteria.get_threshold(self._feature_settings)
count = bit_counts[criteria.bit]
missing_signals = threshold - count

if missing_signals > remaining_blocks:
return BlockIsMissingSignal(feature=feature)

return BlockIsSignaling()

def get_state(self, *, block: 'Block', feature: Feature) -> FeatureState:
"""Returns the state of a feature at a certain block. Uses block metadata to cache states."""

# per definition, the genesis block is in the DEFINED state for all features
Expand All @@ -54,6 +101,9 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
previous_boundary_block = self._get_ancestor_at_height(block=block, height=previous_boundary_height)
previous_boundary_state = self.get_state(block=previous_boundary_block, feature=feature)

# We cache _and save_ the state of the previous boundary block that we just got.
previous_boundary_block.set_feature_state(feature=feature, state=previous_boundary_state, save=True)

if offset_to_boundary != 0:
return previous_boundary_state

Expand All @@ -63,14 +113,16 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
previous_state=previous_boundary_state
)

block.update_feature_state(feature=feature, state=new_state)
# We cache the just calculated state of the current block _without saving it_, as it may still be unverified,
# so we cannot persist its metadata. That's why we cache and save the previous boundary block above.
block.set_feature_state(feature=feature, state=new_state)

return new_state

def _calculate_new_state(
self,
*,
boundary_block: Block,
boundary_block: 'Block',
feature: Feature,
previous_state: FeatureState
) -> FeatureState:
Expand Down Expand Up @@ -136,7 +188,7 @@ def _calculate_new_state(

raise ValueError(f'Unknown previous state: {previous_state}')

def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescription]:
def get_bits_description(self, *, block: 'Block') -> dict[Feature, FeatureDescription]:
"""Returns the criteria definition and feature state for all features at a certain block."""
return {
feature: FeatureDescription(
Expand All @@ -146,7 +198,7 @@ def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescript
for feature, criteria in self._feature_settings.features.items()
}

def _get_ancestor_at_height(self, *, block: Block, height: int) -> Block:
def _get_ancestor_at_height(self, *, block: 'Block', height: int) -> 'Block':
"""
Given a block, returns its ancestor at a specific height.
Uses the height index if the block is in the best blockchain, or search iteratively otherwise.
Expand All @@ -158,13 +210,14 @@ def _get_ancestor_at_height(self, *, block: Block, height: int) -> Block:
metadata = block.get_metadata()

if not metadata.voided_by and (ancestor := self._tx_storage.get_transaction_by_height(height)):
from hathor.transaction import Block
assert isinstance(ancestor, Block)
return ancestor

return _get_ancestor_iteratively(block=block, ancestor_height=height)


def _get_ancestor_iteratively(*, block: Block, ancestor_height: int) -> Block:
def _get_ancestor_iteratively(*, block: 'Block', ancestor_height: int) -> 'Block':
"""Given a block, returns its ancestor at a specific height by iterating over its ancestors. This is slow."""
# TODO: there are further optimizations to be done here, the latest common block height could be persisted in
# metadata, so we could still use the height index if the requested height is before that height.
Expand Down
26 changes: 18 additions & 8 deletions hathor/simulator/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

from hathor.builder import BuildArtifacts, Builder
from hathor.conf.get_settings import get_settings
from hathor.conf.settings import HathorSettings
from hathor.daa import TestMode, _set_test_mode
from hathor.feature_activation.feature_service import FeatureService
from hathor.manager import HathorManager
from hathor.p2p.peer_id import PeerId
from hathor.simulator.clock import HeapClock, MemoryReactorHeapClock
Expand Down Expand Up @@ -168,18 +170,11 @@ def create_artifacts(self, builder: Optional[Builder] = None) -> BuildArtifacts:
wallet = HDWallet(gap_limit=2)
wallet._manually_initialize()

vertex_verifiers = VertexVerifiers(
block=SimulatorBlockVerifier(settings=self.settings),
merge_mined_block=SimulatorMergeMinedBlockVerifier(settings=self.settings),
tx=SimulatorTransactionVerifier(settings=self.settings),
token_creation_tx=SimulatorTokenCreationTransactionVerifier(settings=self.settings),
)

artifacts = builder \
.set_reactor(self._clock) \
.set_rng(Random(self.rng.getrandbits(64))) \
.set_wallet(wallet) \
.set_vertex_verifiers(vertex_verifiers) \
.set_vertex_verifiers_builder(_build_vertex_verifiers) \
.build()

artifacts.manager.start()
Expand Down Expand Up @@ -303,3 +298,18 @@ def run(self,
if trigger is not None:
return False
return True


def _build_vertex_verifiers(settings: HathorSettings, feature_service: FeatureService) -> VertexVerifiers:
"""
A custom VertexVerifiers builder to be used by the simulator.
"""
return VertexVerifiers(
block=SimulatorBlockVerifier(settings=settings, feature_service=feature_service),
merge_mined_block=SimulatorMergeMinedBlockVerifier(
settings=settings,
feature_service=feature_service
),
tx=SimulatorTransactionVerifier(settings=settings),
token_creation_tx=SimulatorTokenCreationTransactionVerifier(settings=settings),
)
18 changes: 0 additions & 18 deletions hathor/transaction/base_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,20 +672,13 @@ def get_metadata(self, *, force_reload: bool = False, use_storage: bool = True)
# happens include generating new mining blocks and some tests
height = self.calculate_height() if self.storage else None
score = self.weight if self.is_genesis else 0
kwargs: dict[str, Any] = {}

if self.is_block:
from hathor.transaction import Block
assert isinstance(self, Block)
kwargs['feature_activation_bit_counts'] = self.calculate_feature_activation_bit_counts()

metadata = TransactionMetadata(
hash=self.hash,
accumulated_weight=self.weight,
height=height,
score=score,
min_height=0,
**kwargs
)
self._metadata = metadata
if not metadata.hash:
Expand Down Expand Up @@ -769,7 +762,6 @@ def update_initial_metadata(self, *, save: bool = True) -> None:
self._update_height_metadata()
self._update_parents_children_metadata()
self._update_reward_lock_metadata()
self._update_feature_activation_bit_counts_metadata()
if save:
assert self.storage is not None
self.storage.save_transaction(self, only_metadata=True)
Expand All @@ -795,16 +787,6 @@ def _update_parents_children_metadata(self) -> None:
metadata.children.append(self.hash)
self.storage.save_transaction(parent, only_metadata=True)

def _update_feature_activation_bit_counts_metadata(self) -> None:
"""Update the block feature_activation_bit_counts metadata."""
if not self.is_block:
return

from hathor.transaction import Block
assert isinstance(self, Block)
metadata = self.get_metadata()
metadata.feature_activation_bit_counts = self.calculate_feature_activation_bit_counts()

def update_timestamp(self, now: int) -> None:
"""Update this tx's timestamp

Expand Down
Loading