Skip to content

Commit bbbbf62

Browse files
authored
python: some refactoring (#442)
1 parent ab5e8ce commit bbbbf62

File tree

8 files changed

+110
-150
lines changed

8 files changed

+110
-150
lines changed

packages/testing/src/consensus_testing/test_fixtures/fork_choice.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,8 @@ def make_fixture(self) -> Self:
218218
#
219219
# The Store is the node's local view of the chain.
220220
# It starts from a trusted anchor (usually genesis).
221-
store = self.anchor_state.to_forkchoice_store(
221+
store = Store.from_anchor(
222+
self.anchor_state,
222223
self.anchor_block,
223224
validator_id=DEFAULT_VALIDATOR_ID,
224225
)

src/lean_spec/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from lean_spec.subspecs.containers.block.types import AggregatedAttestations
3838
from lean_spec.subspecs.containers.slot import Slot
3939
from lean_spec.subspecs.containers.validator import SubnetId
40+
from lean_spec.subspecs.forkchoice import Store
4041
from lean_spec.subspecs.genesis import GenesisConfig
4142
from lean_spec.subspecs.networking.client import LiveNetworkEventSource
4243
from lean_spec.subspecs.networking.enr import ENR
@@ -271,7 +272,7 @@ async def _init_from_checkpoint(
271272
# The store treats this as the new "genesis" for fork choice purposes.
272273
# All blocks before the checkpoint are effectively pruned.
273274
validator_id = validator_registry.primary_index() if validator_registry else None
274-
store = state.to_forkchoice_store(anchor_block, validator_id)
275+
store = Store.from_anchor(state, anchor_block, validator_id)
275276
logger.info(
276277
"Initialized from checkpoint at slot %d (finalized=%s)",
277278
state.slot,

src/lean_spec/subspecs/containers/state/state.py

Lines changed: 28 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
from collections.abc import Set as AbstractSet
77
from typing import TYPE_CHECKING
88

9-
from lean_spec.subspecs.chain.clock import Interval
10-
from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT
119
from lean_spec.subspecs.ssz.hash import hash_tree_root
1210
from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof
1311
from lean_spec.subspecs.xmss.containers import PublicKey, Signature
@@ -35,7 +33,7 @@
3533
)
3634

3735
if TYPE_CHECKING:
38-
from lean_spec.subspecs.forkchoice import GossipSignatureEntry, Store
36+
from lean_spec.subspecs.forkchoice import GossipSignatureEntry
3937

4038

4139
class State(Container):
@@ -81,16 +79,11 @@ def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> State
8179
"""
8280
Generate a genesis state with empty history and proper initial values.
8381
84-
Parameters
85-
----------
86-
genesis_time : Uint64
87-
The genesis timestamp.
88-
validators : Validators
89-
The list of validators in the genesis state.
82+
Args:
83+
genesis_time: The genesis timestamp.
84+
validators: The list of validators in the genesis state.
9085
9186
Returns:
92-
-------
93-
State
9487
A properly initialized genesis state.
9588
"""
9689
# Configure the genesis state.
@@ -121,71 +114,6 @@ def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> State
121114
justifications_validators=JustificationValidators(data=[]),
122115
)
123116

124-
def to_forkchoice_store(
125-
self,
126-
anchor_block: Block,
127-
validator_id: ValidatorIndex | None,
128-
) -> Store:
129-
"""
130-
Initialize a forkchoice store from this state and an anchor block.
131-
132-
The anchor block and this state form the starting point for fork choice.
133-
Both are treated as justified and finalized.
134-
135-
Args:
136-
anchor_block: A trusted block (e.g. genesis or checkpoint).
137-
validator_id: Index of the validator running this store.
138-
139-
Returns:
140-
A new Store instance, ready to accept blocks and attestations.
141-
142-
Raises:
143-
AssertionError:
144-
If the anchor block's state root does not match the hash
145-
of this state.
146-
"""
147-
from lean_spec.subspecs.forkchoice import Store
148-
149-
# Compute the SSZ root of this state.
150-
#
151-
# This is the canonical hash that should appear in the block's state root.
152-
computed_state_root = hash_tree_root(self)
153-
154-
# Check that the block actually points to this state.
155-
#
156-
# If this fails, the caller has supplied inconsistent inputs.
157-
assert anchor_block.state_root == computed_state_root, (
158-
"Anchor block state root must match anchor state hash"
159-
)
160-
161-
# Compute the SSZ root of the anchor block itself.
162-
#
163-
# This root will be used as:
164-
# - the key in the blocks/states maps,
165-
# - the initial head,
166-
# - the root of the initial checkpoints.
167-
anchor_root = hash_tree_root(anchor_block)
168-
169-
# Read the slot at which the anchor block was proposed.
170-
anchor_slot = anchor_block.slot
171-
172-
# Initialize checkpoints from this state.
173-
#
174-
# We explicitly set the root to the anchor block root.
175-
# The state internally might have zero-hash checkpoints (if genesis),
176-
# but the Store must treat the anchor block as the justified/finalized point.
177-
return Store(
178-
time=Interval(anchor_slot * INTERVALS_PER_SLOT),
179-
config=self.config,
180-
head=anchor_root,
181-
safe_target=anchor_root,
182-
latest_justified=self.latest_justified.model_copy(update={"root": anchor_root}),
183-
latest_finalized=self.latest_finalized.model_copy(update={"root": anchor_root}),
184-
blocks={anchor_root: anchor_block},
185-
states={anchor_root: self},
186-
validator_id=validator_id,
187-
)
188-
189117
def process_slots(self, target_slot: Slot) -> State:
190118
"""
191119
Advance the state through empty slots up to, but not including, target_slot.
@@ -195,20 +123,14 @@ def process_slots(self, target_slot: Slot) -> State:
195123
- Increments the slot counter after each call.
196124
The function returns a new state with slot == target_slot.
197125
198-
Parameters
199-
----------
200-
target_slot : Slot
201-
The slot to reach by processing empty slots.
126+
Args:
127+
target_slot: The slot to reach by processing empty slots.
202128
203129
Returns:
204-
-------
205-
State
206130
A new state that has progressed to target_slot.
207131
208132
Raises:
209-
------
210-
AssertionError
211-
If target_slot is not in the future.
133+
AssertionError: If target_slot is not in the future.
212134
"""
213135
# The target must be strictly greater than the current slot.
214136
assert self.slot < target_slot, "Target slot must be in the future"
@@ -278,20 +200,14 @@ def process_block_header(self, block: Block) -> State:
278200
- Insert ZERO_HASH entries for any skipped empty slots.
279201
- Set latest_block_header for the new block with an empty state_root.
280202
281-
Parameters
282-
----------
283-
block : Block
284-
The block whose header is being processed.
203+
Args:
204+
block: The block whose header is being processed.
285205
286206
Returns:
287-
-------
288-
State
289207
A new state with header-related fields updated.
290208
291209
Raises:
292-
------
293-
AssertionError
294-
If any header check fails.
210+
AssertionError: If any header check fails.
295211
"""
296212
# Validation
297213
#
@@ -414,20 +330,15 @@ def process_block(self, block: Block) -> State:
414330
"""
415331
Apply full block processing including header and body.
416332
417-
Parameters
418-
----------
419-
block : Block
420-
The block to process.
333+
Args:
334+
block: The block to process.
421335
422336
Returns:
423-
-------
424-
State
425337
A new state with the processed block.
426338
427339
Raises:
428-
------
429-
AssertionError
430-
If block contains duplicate aggregated attestations with no unique participant.
340+
AssertionError: If block contains duplicate aggregated attestations
341+
with no unique participant.
431342
"""
432343
# First process the block header.
433344
state = self.process_block_header(block)
@@ -447,14 +358,10 @@ def process_attestations(
447358
2. Updates justified status for target checkpoints
448359
3. Applies finalization rules based on justified status
449360
450-
Parameters
451-
----------
452-
attestations : Iterable[AggregatedAttestation]
453-
The aggregated attestations to process.
361+
Args:
362+
attestations: The aggregated attestations to process.
454363
455364
Returns:
456-
-------
457-
State
458365
A new state with updated justification/finalization.
459366
"""
460367
# Reconstruct the vote-tracking structure
@@ -696,22 +603,15 @@ def state_transition(self, block: Block, valid_signatures: bool = True) -> State
696603
3. Process the block header and body
697604
4. Validate the computed state root
698605
699-
Parameters
700-
----------
701-
block : Block
702-
The block to apply to the state.
703-
valid_signatures : bool, optional
704-
Whether to validate block signatures. Defaults to True.
606+
Args:
607+
block: The block to apply to the state.
608+
valid_signatures: Whether to validate block signatures. Defaults to True.
705609
706610
Returns:
707-
-------
708-
State
709611
A new state after applying the block.
710612
711613
Raises:
712-
------
713-
AssertionError
714-
If signature validation fails or state root is invalid.
614+
AssertionError: If signature validation fails or state root is invalid.
715615
"""
716616
# Validate signatures if required
717617
if not valid_signatures:
@@ -871,18 +771,13 @@ def aggregate_gossip_signatures(
871771
from the gossip network. These are fresh signatures that validators
872772
broadcast when they attest.
873773
874-
Parameters
875-
----------
876-
attestations : Collection[Attestation]
877-
Individual attestations to aggregate and sign.
878-
gossip_signatures : dict[AttestationData, set[GossipSignatureEntry]] | None
879-
Per-validator XMSS signatures learned from the gossip network,
880-
keyed by the attestation data they signed.
774+
Args:
775+
attestations: Individual attestations to aggregate and sign.
776+
gossip_signatures: Per-validator XMSS signatures learned from
777+
the gossip network, keyed by the attestation data they signed.
881778
882779
Returns:
883-
-------
884-
list[tuple[AggregatedAttestation, AggregatedSignatureProof]]
885-
- List of (attestation, proof) pairs from gossip collection.
780+
List of (attestation, proof) pairs from gossip collection.
886781
"""
887782
results: list[tuple[AggregatedAttestation, AggregatedSignatureProof]] = []
888783

@@ -955,16 +850,11 @@ def select_aggregated_proofs(
955850
For each attestation group, greedily pick proofs that cover the most
956851
remaining validators until all are covered or no more proofs exist.
957852
958-
Parameters:
959-
----------
960-
attestations : list[Attestation]
961-
Individual attestations to aggregate and sign.
962-
aggregated_payloads : dict[AttestationData, set[AggregatedSignatureProof]] | None
963-
Aggregated proofs keyed by attestation data.
853+
Args:
854+
attestations: Individual attestations to aggregate and sign.
855+
aggregated_payloads: Aggregated proofs keyed by attestation data.
964856
965857
Returns:
966-
-------
967-
tuple[list[AggregatedAttestation], list[AggregatedSignatureProof]]
968858
Paired attestations and their corresponding proofs.
969859
"""
970860
results: list[tuple[AggregatedAttestation, AggregatedSignatureProof]] = []

src/lean_spec/subspecs/forkchoice/store.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
Bytes32,
4242
Uint64,
4343
)
44-
from lean_spec.types.container import Container
44+
from lean_spec.types.base import StrictBaseModel
4545

4646

4747
class GossipSignatureEntry(NamedTuple):
@@ -56,7 +56,7 @@ class GossipSignatureEntry(NamedTuple):
5656
signature: Signature
5757

5858

59-
class Store(Container):
59+
class Store(StrictBaseModel):
6060
"""
6161
Forkchoice store tracking chain state and validator attestations.
6262
@@ -159,6 +159,72 @@ class Store(Container):
159159
Used for recursive signature aggregation when building blocks.
160160
"""
161161

162+
@classmethod
163+
def from_anchor(
164+
cls,
165+
state: State,
166+
anchor_block: Block,
167+
validator_id: ValidatorIndex | None,
168+
) -> "Store":
169+
"""
170+
Initialize a forkchoice store from an anchor state and block.
171+
172+
The anchor block and state form the starting point for fork choice.
173+
Both are treated as justified and finalized.
174+
175+
Args:
176+
state: The post-state of the anchor block.
177+
anchor_block: A trusted block (e.g. genesis or checkpoint).
178+
validator_id: Index of the validator running this store.
179+
180+
Returns:
181+
A new Store instance, ready to accept blocks and attestations.
182+
183+
Raises:
184+
AssertionError:
185+
If the anchor block's state root does not match the hash
186+
of the state.
187+
"""
188+
# Compute the SSZ root of this state.
189+
#
190+
# This is the canonical hash that should appear in the block's state root.
191+
computed_state_root = hash_tree_root(state)
192+
193+
# Check that the block actually points to this state.
194+
#
195+
# If this fails, the caller has supplied inconsistent inputs.
196+
assert anchor_block.state_root == computed_state_root, (
197+
"Anchor block state root must match anchor state hash"
198+
)
199+
200+
# Compute the SSZ root of the anchor block itself.
201+
#
202+
# This root will be used as:
203+
# - the key in the blocks/states maps,
204+
# - the initial head,
205+
# - the root of the initial checkpoints.
206+
anchor_root = hash_tree_root(anchor_block)
207+
208+
# Read the slot at which the anchor block was proposed.
209+
anchor_slot = anchor_block.slot
210+
211+
# Initialize checkpoints from this state.
212+
#
213+
# We explicitly set the root to the anchor block root.
214+
# The state internally might have zero-hash checkpoints (if genesis),
215+
# but the Store must treat the anchor block as the justified/finalized point.
216+
return cls(
217+
time=Interval(anchor_slot * INTERVALS_PER_SLOT),
218+
config=state.config,
219+
head=anchor_root,
220+
safe_target=anchor_root,
221+
latest_justified=state.latest_justified.model_copy(update={"root": anchor_root}),
222+
latest_finalized=state.latest_finalized.model_copy(update={"root": anchor_root}),
223+
blocks={anchor_root: anchor_block},
224+
states={anchor_root: state},
225+
validator_id=validator_id,
226+
)
227+
162228
def prune_stale_attestation_data(self) -> "Store":
163229
"""
164230
Remove attestation data that can no longer influence fork choice.

src/lean_spec/subspecs/node/node.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def from_genesis(cls, config: NodeConfig) -> Node:
212212
# Initialize forkchoice store.
213213
#
214214
# Genesis block is both justified and finalized.
215-
store = state.to_forkchoice_store(block, validator_id)
215+
store = Store.from_anchor(state, block, validator_id)
216216

217217
# Persist genesis to database if available.
218218
#

0 commit comments

Comments
 (0)