Skip to content

Commit 709bf10

Browse files
committed
feat(feature-activation): implement must signal
1 parent d6caa5c commit 709bf10

File tree

15 files changed

+409
-234
lines changed

15 files changed

+409
-234
lines changed

hathor/builder/builder.py

+27-14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class BuildArtifacts(NamedTuple):
6363
pubsub: PubSubManager
6464
consensus: ConsensusAlgorithm
6565
tx_storage: TransactionStorage
66+
feature_service: FeatureService
6667
indexes: Optional[IndexesManager]
6768
wallet: Optional[BaseWallet]
6869
rocksdb_storage: Optional[RocksDBStorage]
@@ -144,7 +145,7 @@ def build(self) -> BuildArtifacts:
144145
if self._network is None:
145146
raise TypeError('you must set a network')
146147

147-
settings = self._get_or_create_settings()
148+
settings = self.get_or_create_settings()
148149
reactor = self._get_reactor()
149150
pubsub = self._get_or_create_pubsub()
150151

@@ -158,9 +159,9 @@ def build(self) -> BuildArtifacts:
158159
wallet = self._get_or_create_wallet()
159160
event_manager = self._get_or_create_event_manager()
160161
indexes = self._get_or_create_indexes_manager()
161-
tx_storage = self._get_or_create_tx_storage(indexes)
162-
feature_service = self._get_or_create_feature_service(tx_storage)
163-
bit_signaling_service = self._get_or_create_bit_signaling_service(tx_storage)
162+
tx_storage = self._get_or_create_tx_storage()
163+
feature_service = self.get_or_create_feature_service()
164+
bit_signaling_service = self._get_or_create_bit_signaling_service()
164165
verification_service = self._get_or_create_verification_service()
165166

166167
if self._enable_address_index:
@@ -221,6 +222,7 @@ def build(self) -> BuildArtifacts:
221222
wallet=wallet,
222223
rocksdb_storage=self._rocksdb_storage,
223224
stratum_factory=stratum_factory,
225+
feature_service=feature_service,
224226
)
225227

226228
return self.artifacts
@@ -264,7 +266,8 @@ def set_peer_id(self, peer_id: PeerId) -> 'Builder':
264266
self._peer_id = peer_id
265267
return self
266268

267-
def _get_or_create_settings(self) -> HathorSettingsType:
269+
def get_or_create_settings(self) -> HathorSettingsType:
270+
"""Return the HathorSettings instance set on this builder, or a new one if not set."""
268271
if self._settings is None:
269272
self._settings = get_settings()
270273
return self._settings
@@ -278,7 +281,7 @@ def _get_soft_voided_tx_ids(self) -> set[bytes]:
278281
if self._soft_voided_tx_ids is not None:
279282
return self._soft_voided_tx_ids
280283

281-
settings = self._get_or_create_settings()
284+
settings = self.get_or_create_settings()
282285

283286
return set(settings.SOFT_VOIDED_TX_IDS)
284287

@@ -352,7 +355,9 @@ def _get_or_create_indexes_manager(self) -> IndexesManager:
352355

353356
return self._indexes_manager
354357

355-
def _get_or_create_tx_storage(self, indexes: IndexesManager) -> TransactionStorage:
358+
def _get_or_create_tx_storage(self) -> TransactionStorage:
359+
indexes = self._get_or_create_indexes_manager()
360+
356361
if self._tx_storage is not None:
357362
# If a tx storage is provided, set the indexes manager to it.
358363
self._tx_storage.indexes = indexes
@@ -408,22 +413,26 @@ def _get_or_create_event_manager(self) -> EventManager:
408413

409414
return self._event_manager
410415

411-
def _get_or_create_feature_service(self, tx_storage: TransactionStorage) -> FeatureService:
416+
def get_or_create_feature_service(self) -> FeatureService:
417+
"""Return the FeatureService instance set on this builder, or a new one if not set."""
412418
if self._feature_service is None:
413-
settings = self._get_or_create_settings()
419+
settings = self.get_or_create_settings()
420+
tx_storage = self._get_or_create_tx_storage()
414421
self._feature_service = FeatureService(
415422
feature_settings=settings.FEATURE_ACTIVATION,
416423
tx_storage=tx_storage
417424
)
418425

419426
return self._feature_service
420427

421-
def _get_or_create_bit_signaling_service(self, tx_storage: TransactionStorage) -> BitSignalingService:
428+
def _get_or_create_bit_signaling_service(self) -> BitSignalingService:
422429
if self._bit_signaling_service is None:
423-
settings = self._get_or_create_settings()
430+
settings = self.get_or_create_settings()
431+
tx_storage = self._get_or_create_tx_storage()
432+
feature_service = self.get_or_create_feature_service()
424433
self._bit_signaling_service = BitSignalingService(
425434
feature_settings=settings.FEATURE_ACTIVATION,
426-
feature_service=self._get_or_create_feature_service(tx_storage),
435+
feature_service=feature_service,
427436
tx_storage=tx_storage,
428437
support_features=self._support_features,
429438
not_support_features=self._not_support_features,
@@ -440,8 +449,12 @@ def _get_or_create_verification_service(self) -> VerificationService:
440449

441450
def _get_or_create_vertex_verifiers(self) -> VertexVerifiers:
442451
if self._vertex_verifiers is None:
443-
settings = self._get_or_create_settings()
444-
self._vertex_verifiers = VertexVerifiers.create_defaults(settings=settings)
452+
settings = self.get_or_create_settings()
453+
feature_service = self.get_or_create_feature_service()
454+
self._vertex_verifiers = VertexVerifiers.create_defaults(
455+
settings=settings,
456+
feature_service=feature_service
457+
)
445458

446459
return self._vertex_verifiers
447460

hathor/builder/cli_builder.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
202202
not_support_features=self._args.signal_not_support
203203
)
204204

205-
vertex_verifiers = VertexVerifiers.create_defaults(settings=settings)
205+
vertex_verifiers = VertexVerifiers.create_defaults(settings=settings, feature_service=self.feature_service)
206206
verification_service = VerificationService(verifiers=vertex_verifiers)
207207

208208
p2p_manager = ConnectionsManager(

hathor/cli/run_node.py

+1
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def prepare(self, *, register_resources: bool = True) -> None:
191191
wallet=self.manager.wallet,
192192
rocksdb_storage=getattr(builder, 'rocksdb_storage', None),
193193
stratum_factory=self.manager.stratum_factory,
194+
feature_service=self.manager._feature_service
194195
)
195196

196197
def start_sentry_if_possible(self) -> None:

hathor/feature_activation/feature_service.py

+44-10
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,56 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import TYPE_CHECKING
16+
1517
from hathor.feature_activation.feature import Feature
1618
from hathor.feature_activation.model.feature_description import FeatureDescription
1719
from hathor.feature_activation.model.feature_state import FeatureState
1820
from hathor.feature_activation.settings import Settings as FeatureSettings
19-
from hathor.transaction import Block
20-
from hathor.transaction.storage import TransactionStorage
21+
22+
if TYPE_CHECKING:
23+
from hathor.transaction import Block
24+
from hathor.transaction.storage import TransactionStorage
2125

2226

2327
class FeatureService:
2428
__slots__ = ('_feature_settings', '_tx_storage')
2529

26-
def __init__(self, *, feature_settings: FeatureSettings, tx_storage: TransactionStorage) -> None:
30+
def __init__(self, *, feature_settings: FeatureSettings, tx_storage: 'TransactionStorage') -> None:
2731
self._feature_settings = feature_settings
2832
self._tx_storage = tx_storage
2933

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

3438
return state == FeatureState.ACTIVE
3539

36-
def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
40+
def check_must_signal(self, block: 'Block') -> bool:
41+
"""Return whether a block is missing mandatory signaling for any feature currently in the MUST_SIGNAL phase."""
42+
bit_counts = block.get_feature_activation_bit_counts()
43+
height = block.get_height()
44+
offset_to_boundary = height % self._feature_settings.evaluation_interval
45+
remaining_blocks = self._feature_settings.evaluation_interval - offset_to_boundary - 1
46+
descriptions = self.get_bits_description(block=block)
47+
48+
must_signal_features = (
49+
feature for feature, description in descriptions.items()
50+
if description.state is FeatureState.MUST_SIGNAL
51+
)
52+
53+
for feature in must_signal_features:
54+
criteria = self._feature_settings.features[feature]
55+
threshold = criteria.get_threshold(self._feature_settings)
56+
count = bit_counts[criteria.bit]
57+
missing_signals = threshold - count
58+
59+
if missing_signals > remaining_blocks:
60+
return True
61+
62+
return False
63+
64+
def get_state(self, *, block: 'Block', feature: Feature) -> FeatureState:
3765
"""Returns the state of a feature at a certain block. Uses block metadata to cache states."""
3866

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

85+
# We cache _and save_ the state of the previous boundary block that we just got.
86+
previous_boundary_block.set_feature_state(feature=feature, state=previous_boundary_state, save=True)
87+
5788
if offset_to_boundary != 0:
5889
return previous_boundary_state
5990

@@ -63,14 +94,16 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
6394
previous_state=previous_boundary_state
6495
)
6596

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

68101
return new_state
69102

70103
def _calculate_new_state(
71104
self,
72105
*,
73-
boundary_block: Block,
106+
boundary_block: 'Block',
74107
feature: Feature,
75108
previous_state: FeatureState
76109
) -> FeatureState:
@@ -136,7 +169,7 @@ def _calculate_new_state(
136169

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

139-
def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescription]:
172+
def get_bits_description(self, *, block: 'Block') -> dict[Feature, FeatureDescription]:
140173
"""Returns the criteria definition and feature state for all features at a certain block."""
141174
return {
142175
feature: FeatureDescription(
@@ -146,7 +179,7 @@ def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescript
146179
for feature, criteria in self._feature_settings.features.items()
147180
}
148181

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

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

164198
return _get_ancestor_iteratively(block=block, ancestor_height=height)
165199

166200

167-
def _get_ancestor_iteratively(*, block: Block, ancestor_height: int) -> Block:
201+
def _get_ancestor_iteratively(*, block: 'Block', ancestor_height: int) -> 'Block':
168202
"""Given a block, returns its ancestor at a specific height by iterating over its ancestors. This is slow."""
169203
# TODO: there are further optimizations to be done here, the latest common block height could be persisted in
170204
# metadata, so we could still use the height index if the requested height is before that height.

hathor/simulator/simulator.py

+15-7
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,25 @@ def create_artifacts(self, builder: Optional[Builder] = None) -> BuildArtifacts:
168168
wallet = HDWallet(gap_limit=2)
169169
wallet._manually_initialize()
170170

171+
builder = builder \
172+
.set_reactor(self._clock) \
173+
.set_rng(Random(self.rng.getrandbits(64))) \
174+
.set_wallet(wallet)
175+
176+
feature_service = builder.get_or_create_feature_service()
177+
settings = builder.get_or_create_settings()
178+
171179
vertex_verifiers = VertexVerifiers(
172-
block=SimulatorBlockVerifier(settings=self.settings),
173-
merge_mined_block=SimulatorMergeMinedBlockVerifier(settings=self.settings),
174-
tx=SimulatorTransactionVerifier(settings=self.settings),
175-
token_creation_tx=SimulatorTokenCreationTransactionVerifier(settings=self.settings),
180+
block=SimulatorBlockVerifier(settings=settings, feature_service=feature_service),
181+
merge_mined_block=SimulatorMergeMinedBlockVerifier(
182+
settings=settings,
183+
feature_service=feature_service
184+
),
185+
tx=SimulatorTransactionVerifier(settings=settings),
186+
token_creation_tx=SimulatorTokenCreationTransactionVerifier(settings=settings),
176187
)
177188

178189
artifacts = builder \
179-
.set_reactor(self._clock) \
180-
.set_rng(Random(self.rng.getrandbits(64))) \
181-
.set_wallet(wallet) \
182190
.set_vertex_verifiers(vertex_verifiers) \
183191
.build()
184192

hathor/transaction/base_transaction.py

-18
Original file line numberDiff line numberDiff line change
@@ -672,20 +672,13 @@ def get_metadata(self, *, force_reload: bool = False, use_storage: bool = True)
672672
# happens include generating new mining blocks and some tests
673673
height = self.calculate_height() if self.storage else None
674674
score = self.weight if self.is_genesis else 0
675-
kwargs: dict[str, Any] = {}
676-
677-
if self.is_block:
678-
from hathor.transaction import Block
679-
assert isinstance(self, Block)
680-
kwargs['feature_activation_bit_counts'] = self.calculate_feature_activation_bit_counts()
681675

682676
metadata = TransactionMetadata(
683677
hash=self.hash,
684678
accumulated_weight=self.weight,
685679
height=height,
686680
score=score,
687681
min_height=0,
688-
**kwargs
689682
)
690683
self._metadata = metadata
691684
if not metadata.hash:
@@ -769,7 +762,6 @@ def update_initial_metadata(self, *, save: bool = True) -> None:
769762
self._update_height_metadata()
770763
self._update_parents_children_metadata()
771764
self._update_reward_lock_metadata()
772-
self._update_feature_activation_bit_counts_metadata()
773765
if save:
774766
assert self.storage is not None
775767
self.storage.save_transaction(self, only_metadata=True)
@@ -795,16 +787,6 @@ def _update_parents_children_metadata(self) -> None:
795787
metadata.children.append(self.hash)
796788
self.storage.save_transaction(parent, only_metadata=True)
797789

798-
def _update_feature_activation_bit_counts_metadata(self) -> None:
799-
"""Update the block feature_activation_bit_counts metadata."""
800-
if not self.is_block:
801-
return
802-
803-
from hathor.transaction import Block
804-
assert isinstance(self, Block)
805-
metadata = self.get_metadata()
806-
metadata.feature_activation_bit_counts = self.calculate_feature_activation_bit_counts()
807-
808790
def update_timestamp(self, now: int) -> None:
809791
"""Update this tx's timestamp
810792

0 commit comments

Comments
 (0)