66from contextlib import contextmanager
77from dataclasses import dataclass , field
88from types import MappingProxyType
9- from typing import Any , cast
9+ from typing import cast
1010
1111from lean_spec .node .chain .clock import SlotClock
1212from lean_spec .node .networking import PeerId
2121from lean_spec .spec .forks import Checkpoint , Slot , ValidatorIndex
2222from lean_spec .spec .forks .lstar import Store
2323from lean_spec .spec .forks .lstar .containers import (
24+ Block ,
2425 SignedAggregatedAttestation ,
2526 SignedAttestation ,
2627 SignedBlock ,
28+ State ,
2729)
2830from lean_spec .spec .forks .lstar .spec import LstarSpec
2931from lean_spec .spec .ssz import Bytes32 , Uint64
3032
3133
32- class StoreInterceptingSpec (LstarSpec ):
33- """Spec stub that delegates consensus calls to the store it receives."""
34-
35- def on_block (self , store : Any , signed_block : Any , * args : Any , ** kwargs : Any ) -> Any :
36- """Delegate block processing to the store."""
37- kwargs .pop ("scheme" , None )
38- return store .on_block (signed_block , * args , ** kwargs )
39-
40- def on_gossip_attestation (
41- self , store : Any , signed_attestation : Any , * args : Any , ** kwargs : Any
42- ) -> Any :
43- """Delegate attestation processing to the store."""
44- kwargs .pop ("scheme" , None )
45- return store .on_gossip_attestation (signed_attestation , * args , ** kwargs )
46-
47- def on_gossip_aggregated_attestation (
48- self , store : Any , signed_attestation : Any , * args : Any , ** kwargs : Any
49- ) -> Any :
50- """Delegate aggregated-attestation processing to the store."""
51- return store .on_gossip_aggregated_attestation (signed_attestation , * args , ** kwargs )
52-
53- def tick_interval (self , store : Any , has_proposal : bool , is_aggregator : bool = False ) -> Any :
54- """Delegate interval ticking to the store."""
55- return store .tick_interval (has_proposal , is_aggregator )
56-
57-
5834@dataclass
5935class MockNetworkRequester :
6036 """Network double that serves pre-loaded blocks and logs every request."""
@@ -148,7 +124,16 @@ class _MockBlock:
148124
149125@dataclass
150126class MockForkchoiceStore :
151- """In-memory forkchoice store double for sync-service tests."""
127+ """
128+ In-memory forkchoice store double for sync-service tests.
129+
130+ One instance plays two roles at once.
131+ It answers reads for the head, blocks, and checkpoints.
132+ It also processes incoming blocks and attestations.
133+
134+ Processing reads its own fields, not a separate store.
135+ So the leading store argument the protocol passes is accepted and ignored.
136+ """
152137
153138 head : Bytes32 = field (default_factory = Bytes32 .zero )
154139 """Root of the head block fork choice currently selects."""
@@ -172,10 +157,10 @@ class MockForkchoiceStore:
172157 )
173158 """Highest finalized checkpoint observed."""
174159
175- blocks : dict [Bytes32 , object ] = field (default_factory = dict )
160+ blocks : dict [Bytes32 , Block | _MockBlock ] = field (default_factory = dict )
176161 """Known blocks, keyed by root."""
177162
178- states : dict [Bytes32 , object ] = field (default_factory = dict )
163+ states : dict [Bytes32 , State ] = field (default_factory = dict )
179164 """Post-state of each block, keyed by root."""
180165
181166 reject_attestation : Callable [[SignedAttestation ], bool ] | None = None
@@ -184,7 +169,7 @@ class MockForkchoiceStore:
184169 reject_aggregated_attestation : Callable [[SignedAggregatedAttestation ], bool ] | None = None
185170 """When it returns true for an aggregate, processing raises instead."""
186171
187- on_block_post_state : object | None = None
172+ on_block_post_state : State | None = None
188173 """Post-state recorded for each processed block, when set."""
189174
190175 advance_justified_on_block : bool = False
@@ -205,26 +190,26 @@ def __post_init__(self) -> None:
205190 """Seed the store with a genesis block stub at the head root."""
206191 self .blocks .setdefault (self .head , _MockBlock (slot = self .head_slot ))
207192
208- def on_block (self , block : SignedBlock , ** kwargs : object ) -> MockForkchoiceStore :
193+ def on_block (self , _store : Store , signed_block : SignedBlock ) -> MockForkchoiceStore :
209194 """Record a block as the new head and apply the configured side effects."""
210- root = hash_tree_root (block .block )
211- self .blocks [root ] = block .block
195+ root = hash_tree_root (signed_block .block )
196+ self .blocks [root ] = signed_block .block
212197 self .head = root
213198 # The double has no real safe-target rule, so the head doubles as it.
214199 self .safe_target = root
215- self .head_slot = block .block .slot
200+ self .head_slot = signed_block .block .slot
216201 if self .on_block_post_state is not None :
217202 self .states [root ] = self .on_block_post_state
218203 if self .advance_justified_on_block :
219- self .latest_justified = Checkpoint (root = root , slot = block .block .slot )
204+ self .latest_justified = Checkpoint (root = root , slot = signed_block .block .slot )
220205 if self .advance_finalized_on_block :
221- self .latest_finalized = Checkpoint (root = root , slot = block .block .slot )
206+ self .latest_finalized = Checkpoint (root = root , slot = signed_block .block .slot )
222207 return self
223208
224209 def on_gossip_attestation (
225210 self ,
211+ _store : Store ,
226212 signed_attestation : SignedAttestation ,
227- * ,
228213 is_aggregator : bool = False ,
229214 ) -> MockForkchoiceStore :
230215 """Record a gossip attestation, unless the reject predicate fires."""
@@ -235,6 +220,7 @@ def on_gossip_attestation(
235220
236221 def on_gossip_aggregated_attestation (
237222 self ,
223+ _store : Store ,
238224 signed_attestation : SignedAggregatedAttestation ,
239225 ) -> MockForkchoiceStore :
240226 """Record an aggregated attestation, unless the reject predicate fires."""
@@ -321,7 +307,7 @@ def put_finalized_checkpoint(self, checkpoint: object) -> None:
321307 """Record a finalized-checkpoint write."""
322308 self ._record ("put_finalized_checkpoint" , checkpoint )
323309
324- def prune_before_slot (self , slot : object , * , keep_roots : frozenset ) -> int :
310+ def prune_before_slot (self , slot : object , * , keep_roots : frozenset [ Bytes32 ] ) -> int :
325311 """Record a prune request and report zero rows removed."""
326312 self ._record ("prune_before_slot" , slot , keep_roots = keep_roots )
327313 return 0
@@ -337,13 +323,18 @@ def create_mock_sync_service(
337323 peer_manager = PeerManager ()
338324 peer_manager .add_peer (PeerInfo (peer_id = peer_id , state = ConnectionState .CONNECTED ))
339325
326+ # One double fills both roles the service expects.
327+ # Its fields answer store reads.
328+ # Its methods answer the processing the service drives.
329+ forkchoice_double = MockForkchoiceStore ()
330+
340331 return SyncService (
341- store = cast (Store , MockForkchoiceStore () ),
332+ store = cast (Store , forkchoice_double ),
342333 peer_manager = peer_manager ,
343334 block_cache = BlockCache (),
344335 clock = SlotClock (genesis_time = Uint64 (0 ), time_fn = lambda : 1000.0 ),
345336 network = MockNetworkRequester (),
346- spec = StoreInterceptingSpec ( ),
337+ spec = cast ( LstarSpec , forkchoice_double ),
347338 database = database ,
348339 genesis_start = genesis_start ,
349340 )
0 commit comments