Skip to content

Commit 607a83d

Browse files
committed
feat(feature-activation): implement must signal
1 parent da50f53 commit 607a83d

File tree

15 files changed

+343
-232
lines changed

15 files changed

+343
-232
lines changed

hathor/builder/builder.py

+24-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,9 @@ 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(settings=settings)
452+
settings = self.get_or_create_settings()
453+
feature_service = self.get_or_create_feature_service()
454+
self._vertex_verifiers = VertexVerifiers.create(settings=settings, feature_service=feature_service)
445455

446456
return self._vertex_verifiers
447457

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(settings=settings)
205+
vertex_verifiers = VertexVerifiers.create(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

+43-10
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,55 @@
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+
descriptions = self.get_bits_description(block=block)
43+
44+
for feature, description in descriptions.items():
45+
criteria = self._feature_settings.features[feature]
46+
47+
if description.state is FeatureState.MUST_SIGNAL:
48+
# TODO: make functions
49+
counts = block.get_feature_activation_bit_counts()
50+
count = counts[criteria.bit]
51+
threshold = criteria.get_threshold(self._feature_settings)
52+
missing_signals = threshold - count
53+
height = block.get_height()
54+
# TODO: Check edges
55+
offset_to_boundary = height % self._feature_settings.evaluation_interval
56+
remaining_blocks = self._feature_settings.evaluation_interval - offset_to_boundary - 1
57+
58+
if missing_signals > remaining_blocks:
59+
return True
60+
61+
return False
62+
63+
def get_state(self, *, block: 'Block', feature: Feature) -> FeatureState:
3764
"""Returns the state of a feature at a certain block. Uses block metadata to cache states."""
3865

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

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

@@ -63,14 +93,16 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
6393
previous_state=previous_boundary_state
6494
)
6595

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

68100
return new_state
69101

70102
def _calculate_new_state(
71103
self,
72104
*,
73-
boundary_block: Block,
105+
boundary_block: 'Block',
74106
feature: Feature,
75107
previous_state: FeatureState
76108
) -> FeatureState:
@@ -136,7 +168,7 @@ def _calculate_new_state(
136168

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

139-
def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescription]:
171+
def get_bits_description(self, *, block: 'Block') -> dict[Feature, FeatureDescription]:
140172
"""Returns the criteria definition and feature state for all features at a certain block."""
141173
return {
142174
feature: FeatureDescription(
@@ -146,7 +178,7 @@ def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescript
146178
for feature, criteria in self._feature_settings.features.items()
147179
}
148180

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

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

164197
return _get_ancestor_iteratively(block=block, ancestor_height=height)
165198

166199

167-
def _get_ancestor_iteratively(*, block: Block, ancestor_height: int) -> Block:
200+
def _get_ancestor_iteratively(*, block: 'Block', ancestor_height: int) -> 'Block':
168201
"""Given a block, returns its ancestor at a specific height by iterating over its ancestors. This is slow."""
169202
# TODO: there are further optimizations to be done here, the latest common block height could be persisted in
170203
# 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)