Skip to content

Commit 7cbfc15

Browse files
tcoratgerclaude
andauthored
refactor(testing): type the sync doubles and delete StoreInterceptingSpec (leanEthereum#914)
The sync-service doubles leaned on Any and object, which hid type errors: the spec stub typed every parameter and return as Any, and the forkchoice store double held its blocks, states, and recorded post-state as object. Two improvements: Type the doubles concretely. The store double now holds blocks as real blocks or the genesis stub, states and the recorded post-state as states, and the prune helper keeps roots as a set of roots. Delete the spec stub entirely. It existed only to forward each consensus call to the store double, paired with a cast in and a cast out per method. The store double now answers both collaborator roles: the sync service receives the one instance as its store and as its spec, so its processing methods take the leading store argument and ignore it. This removes the stub, the casts, and the dead scheme handling. A head-sync test recorded processed blocks as a bare object placeholder; it now records the real block, matching what the store double does. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d85d9e5 commit 7cbfc15

3 files changed

Lines changed: 36 additions & 47 deletions

File tree

packages/testing/src/consensus_testing/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
MockNetworkRequester,
1818
RecordedCall,
1919
RecordingSyncDatabase,
20-
StoreInterceptingSpec,
2120
create_mock_sync_service,
2221
)
2322
from consensus_testing.test_fixtures import (
@@ -209,7 +208,6 @@
209208
"MockNetworkRequester",
210209
"RecordedCall",
211210
"RecordingSyncDatabase",
212-
"StoreInterceptingSpec",
213211
# Base types
214212
"FIXTURE_FORMATS",
215213
"BaseConsensusFixture",

packages/testing/src/consensus_testing/mocks.py

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from contextlib import contextmanager
77
from dataclasses import dataclass, field
88
from types import MappingProxyType
9-
from typing import Any, cast
9+
from typing import cast
1010

1111
from lean_spec.node.chain.clock import SlotClock
1212
from lean_spec.node.networking import PeerId
@@ -21,40 +21,16 @@
2121
from lean_spec.spec.forks import Checkpoint, Slot, ValidatorIndex
2222
from lean_spec.spec.forks.lstar import Store
2323
from lean_spec.spec.forks.lstar.containers import (
24+
Block,
2425
SignedAggregatedAttestation,
2526
SignedAttestation,
2627
SignedBlock,
28+
State,
2729
)
2830
from lean_spec.spec.forks.lstar.spec import LstarSpec
2931
from 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
5935
class MockNetworkRequester:
6036
"""Network double that serves pre-loaded blocks and logs every request."""
@@ -148,7 +124,16 @@ class _MockBlock:
148124

149125
@dataclass
150126
class 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
)

tests/node/sync/test_head_sync.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def track_processing(s: Any, block: SignedBlock) -> Any:
6060
processed_blocks.append(root)
6161
new_store = MockForkchoiceStore()
6262
new_store.blocks = dict(s.blocks)
63-
new_store.blocks[root] = object()
63+
new_store.blocks[root] = block.block
6464
return new_store
6565

6666
head_sync = HeadSync(
@@ -196,7 +196,7 @@ def track_processing(s: Any, block: SignedBlock) -> Any:
196196
processing_order.append(root)
197197
new_store = MockForkchoiceStore()
198198
new_store.blocks = dict(s.blocks)
199-
new_store.blocks[root] = object()
199+
new_store.blocks[root] = block.block
200200
return new_store
201201

202202
head_sync = HeadSync(
@@ -249,7 +249,7 @@ def track_processing(s: Any, block: SignedBlock) -> Any:
249249
root = hash_tree_root(block.block)
250250
new_store = MockForkchoiceStore()
251251
new_store.blocks = dict(s.blocks)
252-
new_store.blocks[root] = object()
252+
new_store.blocks[root] = block.block
253253
return new_store
254254

255255
head_sync = HeadSync(
@@ -352,7 +352,7 @@ def track_processing(s: Any, block: SignedBlock) -> Any:
352352
root = hash_tree_root(block.block)
353353
new_store = MockForkchoiceStore()
354354
new_store.blocks = dict(s.blocks)
355-
new_store.blocks[root] = object()
355+
new_store.blocks[root] = block.block
356356
return new_store
357357

358358
head_sync = HeadSync(

0 commit comments

Comments
 (0)