Skip to content

Commit a9c8750

Browse files
tcoratgerclaude
andauthored
refactor(forks): make Store generic over StateT, BlockT (Stage 5 of leanEthereum#686) (leanEthereum#705)
Per the multi-fork roadmap, Store now declares its State and Block types as type parameters so future forks can specialize them with a typedef instead of copy-pasting the whole class. Design (per the py-architect agent): - `Store(StrictBaseModel, Generic[StateT, BlockT])` in forks/lstar/store.py. StateT and BlockT are bound to Container so Pydantic can build a real schema at parameterization time; structural protocols would not. - `LstarStore = Store[State, Block]` in forks/lstar/spec.py is the concrete binding owned by the lstar fork. `LstarSpec.store_class` and `create_store` are typed against it. - `BlockLookup` is dropped — it was a single-line alias used in two helper signatures that read just as clearly as `dict[Bytes32, Block]`. - Public `from lean_spec.forks import Store` resolves to `LstarStore`, so every existing call site keeps working without change. `LstarStore` is also exported under its canonical name for callers that prefer to be explicit. - Mutable Store defaults move from bare `= {}` to `Field(default_factory= dict)` to silence the Pydantic generic-default warning ty raised intermittently and to keep the schema honest. The third Stage 5 item (a `Devnet5Store` typedef demonstrating the pattern) is dropped because devnet5 was unified back into lstar. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 45ba21e commit a9c8750

8 files changed

Lines changed: 63 additions & 51 deletions

File tree

packages/testing/src/consensus_testing/test_types/store_checks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Literal
44

55
from lean_spec.forks.lstar.containers import AttestationData
6-
from lean_spec.forks.lstar.containers.block.block import Block, BlockLookup
6+
from lean_spec.forks.lstar.containers.block.block import Block
77
from lean_spec.forks.lstar.spec import LstarSpec
88
from lean_spec.forks.lstar.store import Store
99
from lean_spec.subspecs.ssz import hash_tree_root
@@ -12,7 +12,7 @@
1212
from .utils import resolve_block_root
1313

1414

15-
def _ancestor_set(blocks: BlockLookup, head: Bytes32) -> set[Bytes32]:
15+
def _ancestor_set(blocks: dict[Bytes32, Block], head: Bytes32) -> set[Bytes32]:
1616
"""Walk parent links from head and collect every reachable block root."""
1717
seen: set[Bytes32] = set()
1818
root = head

src/lean_spec/forks/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
SignedBlock,
1414
Validator,
1515
)
16-
from .lstar.containers.block import BlockLookup, BlockSignatures
16+
from .lstar.containers.block import BlockSignatures
1717
from .lstar.containers.block.types import AggregatedAttestations, AttestationSignatures
1818
from .lstar.containers.state import State, Validators
19-
from .lstar.spec import LstarSpec
20-
from .lstar.store import AttestationSignatureEntry, Store
19+
from .lstar.spec import LstarSpec, LstarStore
20+
from .lstar.store import AttestationSignatureEntry
2121
from .protocol import ForkProtocol, SpecStateType, SpecStoreType
2222
from .registry import ForkRegistry
2323

24+
Store = LstarStore
25+
"""Public alias resolving to the concrete LstarStore until other forks land."""
26+
2427
FORK_SEQUENCE: list[ForkProtocol] = [LstarSpec()]
2528
"""Ordered oldest to newest. ForkRegistry enforces strictly increasing VERSION."""
2629

@@ -37,14 +40,14 @@
3740
"Block",
3841
"BlockBody",
3942
"BlockHeader",
40-
"BlockLookup",
4143
"BlockSignatures",
4244
"Config",
4345
"DEFAULT_REGISTRY",
4446
"FORK_SEQUENCE",
4547
"ForkProtocol",
4648
"ForkRegistry",
4749
"LstarSpec",
50+
"LstarStore",
4851
"SignedAggregatedAttestation",
4952
"SignedAttestation",
5053
"SignedBlock",
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Lstar fork"""
22

33
from .containers.state import State
4-
from .store import AttestationSignatureEntry, Store
4+
from .spec import LstarStore as Store
5+
from .store import AttestationSignatureEntry
56

67
__all__ = ["AttestationSignatureEntry", "State", "Store"]

src/lean_spec/forks/lstar/containers/block/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
Block,
55
BlockBody,
66
BlockHeader,
7-
BlockLookup,
87
BlockSignatures,
98
SignedBlock,
109
)
@@ -17,7 +16,6 @@
1716
"Block",
1817
"BlockBody",
1918
"BlockHeader",
20-
"BlockLookup",
2119
"BlockSignatures",
2220
"SignedBlock",
2321
"AggregatedAttestations",

src/lean_spec/forks/lstar/containers/block/block.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ class Block(Container):
6666
"""The block's payload."""
6767

6868

69-
BlockLookup = dict[Bytes32, Block]
70-
"""Mapping from block root to Block objects."""
71-
72-
7369
class BlockSignatures(Container):
7470
"""Aggregated signature payload for a block."""
7571

src/lean_spec/forks/lstar/spec.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
from ..protocol import ForkProtocol, SpecBlockType, SpecStateType
6363
from .store import AttestationSignatureEntry, Store
6464

65+
LstarStore = Store[State, Block]
66+
"""Concrete Store specialization owned by the lstar fork."""
67+
6568

6669
class LstarSpec(ForkProtocol):
6770
"""Lstar fork."""
@@ -80,7 +83,7 @@ class LstarSpec(ForkProtocol):
8083
block_signatures_class: type[BlockSignatures] = BlockSignatures
8184
aggregated_attestations_class: type[AggregatedAttestations] = AggregatedAttestations
8285
attestation_signatures_class: type[AttestationSignatures] = AttestationSignatures
83-
store_class: type[Store] = Store
86+
store_class: type[Store[State, Block]] = LstarStore
8487

8588
attestation_data_class: type[AttestationData] = AttestationData
8689
attestation_class: type[Attestation] = Attestation
@@ -878,7 +881,7 @@ def create_store( # type: ignore[override] # ty: ignore[invalid-method-overrid
878881
state: SpecStateType,
879882
anchor_block: SpecBlockType,
880883
validator_id: ValidatorIndex | None,
881-
) -> Store:
884+
) -> LstarStore:
882885
"""Initialize a forkchoice store from an anchor state and block.
883886
884887
The anchor block and state form the starting point for fork choice.
@@ -935,7 +938,7 @@ def create_store( # type: ignore[override] # ty: ignore[invalid-method-overrid
935938
validator_id=validator_id,
936939
)
937940

938-
def prune_stale_attestation_data(self, store: Store) -> Store:
941+
def prune_stale_attestation_data(self, store: LstarStore) -> LstarStore:
939942
"""Remove attestation data that can no longer influence fork choice.
940943
941944
An attestation becomes stale when its target checkpoint falls at or before
@@ -972,7 +975,7 @@ def prune_stale_attestation_data(self, store: Store) -> Store:
972975
}
973976
)
974977

975-
def validate_attestation(self, store: Store, attestation_data: AttestationData) -> None:
978+
def validate_attestation(self, store: LstarStore, attestation_data: AttestationData) -> None:
976979
"""Validate incoming attestation before processing.
977980
978981
Ensures the vote respects the basic laws of time and topology:
@@ -1026,11 +1029,11 @@ def validate_attestation(self, store: Store, attestation_data: AttestationData)
10261029

10271030
def on_gossip_attestation(
10281031
self,
1029-
store: Store,
1032+
store: LstarStore,
10301033
signed_attestation: SignedAttestation,
10311034
scheme: GeneralizedXmssScheme = TARGET_SIGNATURE_SCHEME,
10321035
is_aggregator: bool = False,
1033-
) -> Store:
1036+
) -> LstarStore:
10341037
"""Process a signed attestation received via gossip network.
10351038
10361039
This method:
@@ -1090,9 +1093,9 @@ def on_gossip_attestation(
10901093

10911094
def on_gossip_aggregated_attestation(
10921095
self,
1093-
store: Store,
1096+
store: LstarStore,
10941097
signed_attestation: SignedAggregatedAttestation,
1095-
) -> Store:
1098+
) -> LstarStore:
10961099
"""Process a signed aggregated attestation received via aggregation topic.
10971100
10981101
This method:
@@ -1155,10 +1158,10 @@ def on_gossip_aggregated_attestation(
11551158

11561159
def on_block(
11571160
self,
1158-
store: Store,
1161+
store: LstarStore,
11591162
signed_block: SignedBlock,
11601163
scheme: GeneralizedXmssScheme = TARGET_SIGNATURE_SCHEME,
1161-
) -> Store:
1164+
) -> LstarStore:
11621165
"""Process a new block and update the forkchoice state.
11631166
11641167
This method integrates a block into the forkchoice store by:
@@ -1263,7 +1266,7 @@ def on_block(
12631266

12641267
def extract_attestations_from_aggregated_payloads(
12651268
self,
1266-
store: Store,
1269+
store: LstarStore,
12671270
aggregated_payloads: dict[AttestationData, set[AggregatedSignatureProof]],
12681271
) -> dict[ValidatorIndex, AttestationData]:
12691272
"""Extract attestations from aggregated payloads.
@@ -1281,7 +1284,7 @@ def extract_attestations_from_aggregated_payloads(
12811284
attestations[validator_id] = attestation_data
12821285
return attestations
12831286

1284-
def compute_block_weights(self, store: Store) -> dict[Bytes32, int]:
1287+
def compute_block_weights(self, store: LstarStore) -> dict[Bytes32, int]:
12851288
"""Compute attestation-based weight for each block above the finalized slot.
12861289
12871290
Walks backward from each validator's latest head vote, incrementing weight
@@ -1306,7 +1309,7 @@ def compute_block_weights(self, store: Store) -> dict[Bytes32, int]:
13061309

13071310
def _compute_lmd_ghost_head(
13081311
self,
1309-
store: Store,
1312+
store: LstarStore,
13101313
start_root: Bytes32,
13111314
attestations: dict[ValidatorIndex, AttestationData],
13121315
min_score: int = 0,
@@ -1388,7 +1391,7 @@ def _compute_lmd_ghost_head(
13881391

13891392
return head
13901393

1391-
def update_head(self, store: Store) -> Store:
1394+
def update_head(self, store: LstarStore) -> LstarStore:
13921395
"""Compute updated store with new canonical head.
13931396
13941397
Selects the canonical chain head using:
@@ -1418,7 +1421,7 @@ def update_head(self, store: Store) -> Store:
14181421
}
14191422
)
14201423

1421-
def accept_new_attestations(self, store: Store) -> Store:
1424+
def accept_new_attestations(self, store: LstarStore) -> LstarStore:
14221425
"""Process pending aggregated payloads and update forkchoice head.
14231426
14241427
Moves aggregated payloads from latest_new_aggregated_payloads to
@@ -1456,7 +1459,7 @@ def accept_new_attestations(self, store: Store) -> Store:
14561459
# Update head with newly accepted aggregated payloads
14571460
return self.update_head(store)
14581461

1459-
def update_safe_target(self, store: Store) -> Store:
1462+
def update_safe_target(self, store: LstarStore) -> LstarStore:
14601463
"""Compute the deepest block that has 2/3+ supermajority attestation weight.
14611464
14621465
The safe target is the furthest-from-genesis block where enough validators
@@ -1523,7 +1526,7 @@ def update_safe_target(self, store: Store) -> Store:
15231526
# The head and attestation pools remain unchanged.
15241527
return store.model_copy(update={"safe_target": safe_target})
15251528

1526-
def aggregate(self, store: Store) -> tuple[Store, list[SignedAggregatedAttestation]]:
1529+
def aggregate(self, store: LstarStore) -> tuple[LstarStore, list[SignedAggregatedAttestation]]:
15271530
"""Turn raw validator votes into compact aggregated attestations.
15281531
15291532
Validators cast individual signatures over gossip. Before those
@@ -1660,10 +1663,10 @@ def aggregate(self, store: Store) -> tuple[Store, list[SignedAggregatedAttestati
16601663

16611664
def tick_interval(
16621665
self,
1663-
store: Store,
1666+
store: LstarStore,
16641667
has_proposal: bool,
16651668
is_aggregator: bool = False,
1666-
) -> tuple[Store, list[SignedAggregatedAttestation]]:
1669+
) -> tuple[LstarStore, list[SignedAggregatedAttestation]]:
16671670
"""Advance store time by one interval and perform interval-specific actions.
16681671
16691672
Different actions are performed based on interval within slot:
@@ -1691,11 +1694,11 @@ def tick_interval(
16911694

16921695
def on_tick(
16931696
self,
1694-
store: Store,
1697+
store: LstarStore,
16951698
target_interval: Interval,
16961699
has_proposal: bool,
16971700
is_aggregator: bool = False,
1698-
) -> tuple[Store, list[SignedAggregatedAttestation]]:
1701+
) -> tuple[LstarStore, list[SignedAggregatedAttestation]]:
16991702
"""Advance forkchoice store time to given interval count.
17001703
17011704
Ticks store forward interval by interval, performing appropriate
@@ -1716,7 +1719,7 @@ def on_tick(
17161719

17171720
return store, all_new_aggregates
17181721

1719-
def get_proposal_head(self, store: Store, slot: Slot) -> tuple[Store, Bytes32]:
1722+
def get_proposal_head(self, store: LstarStore, slot: Slot) -> tuple[LstarStore, Bytes32]:
17201723
"""Get the head for block proposal at given slot.
17211724
17221725
Ensures store is up-to-date and processes any pending attestations
@@ -1732,7 +1735,7 @@ def get_proposal_head(self, store: Store, slot: Slot) -> tuple[Store, Bytes32]:
17321735

17331736
return store, store.head
17341737

1735-
def get_attestation_target(self, store: Store) -> Checkpoint:
1738+
def get_attestation_target(self, store: LstarStore) -> Checkpoint:
17361739
"""Calculate target checkpoint for validator attestations.
17371740
17381741
Determines appropriate attestation target based on head, safe target,
@@ -1770,7 +1773,7 @@ def get_attestation_target(self, store: Store) -> Checkpoint:
17701773

17711774
return Checkpoint(root=target_block_root, slot=target_block.slot)
17721775

1773-
def produce_attestation_data(self, store: Store, slot: Slot) -> AttestationData:
1776+
def produce_attestation_data(self, store: LstarStore, slot: Slot) -> AttestationData:
17741777
"""Produce attestation data for the given slot.
17751778
17761779
This method constructs an AttestationData object according to the lean protocol
@@ -1796,10 +1799,10 @@ def produce_attestation_data(self, store: Store, slot: Slot) -> AttestationData:
17961799

17971800
def produce_block_with_signatures(
17981801
self,
1799-
store: Store,
1802+
store: LstarStore,
18001803
slot: Slot,
18011804
validator_index: ValidatorIndex,
1802-
) -> tuple[Store, Block, list[AggregatedSignatureProof]]:
1805+
) -> tuple[LstarStore, Block, list[AggregatedSignatureProof]]:
18031806
"""Produce a block and its aggregated signature proofs for the target slot.
18041807
18051808
Block production proceeds in four stages:

src/lean_spec/forks/lstar/store.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,28 @@
44
The Store tracks all information required for the LMD GHOST forkchoice algorithm.
55
"""
66

7-
__all__ = ["AttestationSignatureEntry", "Store"]
7+
__all__ = ["AttestationSignatureEntry", "BlockT", "StateT", "Store"]
88

9-
from typing import NamedTuple
9+
from typing import Generic, NamedTuple, TypeVar
10+
11+
from pydantic import Field
1012

1113
from lean_spec.forks.lstar.containers import (
1214
AttestationData,
1315
Config,
1416
)
15-
from lean_spec.forks.lstar.containers.block import BlockLookup
1617
from lean_spec.subspecs.chain.clock import Interval
1718
from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof
1819
from lean_spec.subspecs.xmss.containers import Signature
1920
from lean_spec.types import Bytes32, Checkpoint, ValidatorIndex
2021
from lean_spec.types.base import StrictBaseModel
22+
from lean_spec.types.container import Container
23+
24+
StateT = TypeVar("StateT", bound=Container)
25+
"""Per-fork post-state type tracked alongside each known block."""
2126

22-
from .containers.state import State
27+
BlockT = TypeVar("BlockT", bound=Container)
28+
"""Per-fork block type stored in the forkchoice view."""
2329

2430

2531
class AttestationSignatureEntry(NamedTuple):
@@ -34,7 +40,7 @@ class AttestationSignatureEntry(NamedTuple):
3440
signature: Signature
3541

3642

37-
class Store(StrictBaseModel):
43+
class Store(StrictBaseModel, Generic[StateT, BlockT]):
3844
"""
3945
Forkchoice store tracking chain state and validator attestations.
4046
@@ -91,7 +97,7 @@ class Store(StrictBaseModel):
9197
Fork choice will never revert finalized history.
9298
"""
9399

94-
blocks: BlockLookup = {}
100+
blocks: dict[Bytes32, BlockT] = Field(default_factory=dict)
95101
"""
96102
Mapping from block root to Block objects.
97103
@@ -100,7 +106,7 @@ class Store(StrictBaseModel):
100106
Every block that might participate in fork choice must appear here.
101107
"""
102108

103-
states: dict[Bytes32, State] = {}
109+
states: dict[Bytes32, StateT] = Field(default_factory=dict)
104110
"""
105111
Mapping from block root to State objects.
106112
@@ -113,14 +119,18 @@ class Store(StrictBaseModel):
113119
validator_id: ValidatorIndex | None
114120
"""Index of the validator running this store instance."""
115121

116-
attestation_signatures: dict[AttestationData, set[AttestationSignatureEntry]] = {}
122+
attestation_signatures: dict[AttestationData, set[AttestationSignatureEntry]] = Field(
123+
default_factory=dict
124+
)
117125
"""
118126
Per-validator XMSS signatures learned from committee attesters.
119127
120128
Keyed by AttestationData.
121129
"""
122130

123-
latest_new_aggregated_payloads: dict[AttestationData, set[AggregatedSignatureProof]] = {}
131+
latest_new_aggregated_payloads: dict[AttestationData, set[AggregatedSignatureProof]] = Field(
132+
default_factory=dict
133+
)
124134
"""
125135
Aggregated signature proofs pending processing.
126136
@@ -129,7 +139,9 @@ class Store(StrictBaseModel):
129139
Populated from blocks or gossip aggregated attestations.
130140
"""
131141

132-
latest_known_aggregated_payloads: dict[AttestationData, set[AggregatedSignatureProof]] = {}
142+
latest_known_aggregated_payloads: dict[AttestationData, set[AggregatedSignatureProof]] = Field(
143+
default_factory=dict
144+
)
133145
"""
134146
Aggregated signature proofs that have been processed.
135147

0 commit comments

Comments
 (0)