Skip to content

Commit ebaf6bc

Browse files
tcoratgerclaude
andauthored
refactor(testing): consolidate consensus_testing genesis builders (leanEthereum#1153)
* refactor(testing): consolidate consensus_testing genesis builders Collapse the genesis/anchor construction helpers into a small, consistent surface and remove duplication. - Merge the keyed and zeroed validator/state builders into one build_genesis_state(..., keyed=...); drop generate_pre_state, make_genesis_state, make_validators, _build_validators. - Rename make_genesis_store to build_genesis_store and unify the build_ verb. - Inline the trivial genesis-block reconstruction at its call sites and remove reconstruct_block_from_header. - Let build_anchor handle slot 0 as the genesis case so build_genesis_store delegates to it; the genesis-block construction now lives in one place. - Inline the former module-level defaults and default fork directly in signatures; whitelist the immutable Uint64/ValidatorIndex/LstarSpec calls in ruff's flake8-bugbear config. - Update all call sites across tests/ and packages/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(testing): restore reconstruct_block_from_header helper The inlined genesis-block construction hurt readability across the fixtures and tests. Bring the helper back, export it, and call it again everywhere the block is rebuilt from a state's latest header, including inside build_anchor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7823bd9 commit ebaf6bc

39 files changed

Lines changed: 247 additions & 300 deletions

packages/testing/src/consensus_testing/__init__.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
from consensus_testing import forks
66
from consensus_testing.genesis import (
77
build_anchor,
8-
generate_pre_state,
9-
make_genesis_state,
10-
make_genesis_store,
11-
make_validators,
8+
build_genesis_state,
9+
build_genesis_store,
1210
reconstruct_block_from_header,
1311
)
1412
from consensus_testing.mocks import (
@@ -197,12 +195,10 @@
197195
"BlockSpec",
198196
"forks",
199197
"build_anchor",
200-
"generate_pre_state",
198+
"build_genesis_state",
201199
# Unit-test builders and value constructors
202200
"reconstruct_block_from_header",
203-
"make_genesis_state",
204-
"make_genesis_store",
205-
"make_validators",
201+
"build_genesis_store",
206202
"create_mock_sync_service",
207203
"TEST_VALIDATOR_INDEX",
208204
"make_signed_attestation",
Lines changed: 76 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Consensus layer pre-state generation."""
1+
"""Consensus layer genesis state, block, and anchor construction for tests."""
22

33
from consensus_testing.keys import XmssKeyManager
44
from lean_spec.spec.crypto.merkleization import hash_tree_root
@@ -18,111 +18,98 @@
1818
from lean_spec.spec.forks.lstar.spec import LstarSpec
1919
from lean_spec.spec.ssz import Bytes52, Uint64
2020

21-
_DEFAULT_GENESIS_TIME = Uint64(0)
2221

23-
_DEFAULT_VALIDATOR_INDEX = ValidatorIndex(0)
24-
"""Owning validator for a genesis store, unless overridden."""
25-
26-
27-
def _build_validators(num_validators: int) -> Validators:
28-
"""Build a validator registry with real XMSS keys from the shared key manager."""
29-
key_manager = XmssKeyManager.shared()
22+
def build_genesis_state(
23+
num_validators: int = 4,
24+
*,
25+
genesis_time: Uint64 = Uint64(0),
26+
keyed: bool = True,
27+
fork: LstarSpec = LstarSpec(),
28+
) -> State:
29+
"""
30+
Build a genesis pre-state for consensus tests.
3031
31-
if num_validators > len(key_manager):
32-
raise ValueError(
33-
f"Not enough keys: need {num_validators} validators "
34-
f"but the key manager has only {len(key_manager)} keys"
35-
)
32+
Keyed validators get real signing keys from the shared key manager.
33+
Unkeyed validators get zeroed keys, for tests that never check signatures.
3634
37-
validators = []
38-
for validator_position in range(num_validators):
39-
validator_index = ValidatorIndex(validator_position)
40-
attestation_public_key, proposal_public_key = key_manager.get_public_keys(validator_index)
41-
validators.append(
35+
Raises:
36+
ValueError: If keyed and the key manager holds fewer keys than requested.
37+
"""
38+
if keyed:
39+
key_manager = XmssKeyManager.shared()
40+
if num_validators > len(key_manager):
41+
raise ValueError(
42+
f"Not enough keys: need {num_validators} validators "
43+
f"but the key manager has only {len(key_manager)} keys"
44+
)
45+
validators = []
46+
for validator_position in range(num_validators):
47+
validator_index = ValidatorIndex(validator_position)
48+
attestation_public_key, proposal_public_key = key_manager.get_public_keys(
49+
validator_index
50+
)
51+
validators.append(
52+
Validator(
53+
attestation_public_key=Bytes52(attestation_public_key.encode_bytes()),
54+
proposal_public_key=Bytes52(proposal_public_key.encode_bytes()),
55+
index=validator_index,
56+
)
57+
)
58+
else:
59+
validators = [
4260
Validator(
43-
attestation_public_key=Bytes52(attestation_public_key.encode_bytes()),
44-
proposal_public_key=Bytes52(proposal_public_key.encode_bytes()),
45-
index=validator_index,
61+
attestation_public_key=Bytes52(b"\x00" * 52),
62+
proposal_public_key=Bytes52(b"\x00" * 52),
63+
index=ValidatorIndex(validator_position),
4664
)
47-
)
65+
for validator_position in range(num_validators)
66+
]
4867

49-
return Validators(data=validators)
68+
return fork.generate_genesis(
69+
genesis_time=genesis_time,
70+
validators=Validators(data=validators),
71+
)
5072

5173

52-
def generate_pre_state(
53-
fork: LstarSpec | None = None,
54-
genesis_time: Uint64 = _DEFAULT_GENESIS_TIME,
55-
num_validators: int = 4,
56-
) -> State:
74+
def reconstruct_block_from_header(state: State) -> Block:
5775
"""
58-
Generate a default pre-state for consensus tests.
59-
60-
Args:
61-
fork: Fork dispatching genesis construction. Defaults to a fresh
62-
LstarSpec instance.
63-
genesis_time: The genesis timestamp.
64-
num_validators: Number of validators to include.
76+
Rebuild the block matching a state's latest header.
6577
66-
Returns:
67-
A properly initialized consensus state.
78+
The body is empty by the genesis and empty-block convention.
79+
For a genesis state this is the genesis block.
6880
"""
69-
fork = fork or LstarSpec()
70-
validators = _build_validators(num_validators)
71-
return fork.generate_genesis(genesis_time=genesis_time, validators=validators)
81+
return Block(
82+
slot=state.latest_block_header.slot,
83+
proposer_index=state.latest_block_header.proposer_index,
84+
parent_root=state.latest_block_header.parent_root,
85+
state_root=hash_tree_root(state),
86+
body=BlockBody(attestations=AggregatedAttestations(data=[])),
87+
)
7288

7389

7490
def build_anchor(
7591
num_validators: int,
7692
anchor_slot: Slot,
77-
fork: LstarSpec | None = None,
78-
genesis_time: Uint64 = _DEFAULT_GENESIS_TIME,
93+
*,
94+
fork: LstarSpec = LstarSpec(),
95+
genesis_time: Uint64 = Uint64(0),
96+
keyed: bool = True,
7997
synced: bool = False,
8098
) -> tuple[State, Block]:
8199
"""
82-
Build a non-genesis anchor by advancing the genesis state to a slot.
83-
84-
By default the anchor keeps the genesis checkpoints, modelling a mid-chain
85-
state that has not finalized anything yet.
100+
Build an anchor by advancing the genesis state through a slot.
86101
87-
With synced set, it models a checkpoint-synced node instead: both checkpoints
88-
pin to the anchor slot and the justification window rebases onto that boundary.
89-
90-
Either way the returned pair is internally consistent.
91-
The block state root equals the hash of the state.
92-
93-
Args:
94-
num_validators: Size of the validator set in the anchor state.
95-
anchor_slot: Slot at which the anchor block lives. Must be > 0.
96-
fork: Fork dispatching genesis construction.
97-
genesis_time: Genesis timestamp for the underlying pre-state.
98-
synced: Pin both checkpoints to the anchor slot, for checkpoint-sync vectors.
99-
100-
Returns:
101-
A tuple of (anchor_state, anchor_block).
102-
103-
Raises:
104-
ValueError: If anchor_slot is not strictly positive.
102+
At slot 0 the advance loop is empty, so this returns the genesis pair.
105103
"""
106-
if anchor_slot <= Slot(0):
107-
raise ValueError(
108-
f"Anchor slot must be strictly positive, got {anchor_slot}. "
109-
"For a genesis anchor use generate_pre_state instead."
110-
)
111-
112-
fork = fork or LstarSpec()
113-
state = generate_pre_state(fork=fork, genesis_time=genesis_time, num_validators=num_validators)
104+
state = build_genesis_state(num_validators, genesis_time=genesis_time, keyed=keyed, fork=fork)
114105

115-
# Reconstruct the genesis block from the state's latest header.
116-
# The genesis block is fully determined by the genesis state.
117106
current_block = reconstruct_block_from_header(state)
118107
parent_root = hash_tree_root(current_block)
119108

120109
num_validators_u64 = Uint64(num_validators)
121110

122-
# Advance through empty blocks, one per slot, up to and including anchor_slot.
123-
# Each block is built by the spec's own builder so the resulting state
124-
# carries the real chain history (historical block hashes, justified slots,
125-
# justification tracking) that a real mid-chain state would have.
111+
# Advance one empty block per slot, up to and including the anchor.
112+
# Using the spec's own builder gives the state real mid-chain history.
126113
for next_slot in range(1, int(anchor_slot) + 1):
127114
slot = Slot(next_slot)
128115
proposer_index = ValidatorIndex.proposer_for_slot(slot, num_validators_u64)
@@ -139,19 +126,13 @@ def build_anchor(
139126
if not synced:
140127
return state, current_block
141128

142-
# Rebase the state onto the anchor as a freshly checkpoint-synced node would see it.
143-
#
144-
# The empty-block advance leaves both checkpoints at the genesis boundary (slot 0).
145-
# A node that syncs from this anchor trusts it as finalized at the anchor slot.
146-
# So both checkpoints move to the anchor block at the anchor slot.
129+
# Rebase the state as a freshly checkpoint-synced node would see it.
130+
# Such a node trusts the anchor as finalized, so both checkpoints move there.
147131
anchor_root = hash_tree_root(current_block)
148132
anchor_checkpoint = Checkpoint(root=anchor_root, slot=anchor_slot)
149133

150-
# The justified-slots window is stored relative to the finalized boundary.
151-
#
152-
# Its first bit is the slot just after finalization.
153-
# Moving the boundary forward by the anchor slot drops that many leading bits.
154-
# No slot beyond the anchor is materialized, so the rebased window is empty.
134+
# The justified-slots window starts at the slot after the finalized boundary.
135+
# Moving that boundary to the anchor drops the leading bits; nothing past it exists.
155136
rebase_distance = int(anchor_slot - state.latest_finalized.slot)
156137
rebased_justified_slots = JustifiedSlots(data=state.justified_slots.data[rebase_distance:])
157138

@@ -172,70 +153,27 @@ def build_anchor(
172153
return state, current_block
173154

174155

175-
def make_validators(count: int) -> Validators:
176-
"""Build a validator registry of the given size with zeroed public keys."""
177-
return Validators(
178-
data=[
179-
Validator(
180-
attestation_public_key=Bytes52(b"\x00" * 52),
181-
proposal_public_key=Bytes52(b"\x00" * 52),
182-
index=ValidatorIndex(validator_position),
183-
)
184-
for validator_position in range(count)
185-
]
186-
)
187-
188-
189-
def make_genesis_state(num_validators: int = 3, genesis_time: int = 0) -> State:
190-
"""Build a genesis state with zeroed validator keys."""
191-
return LstarSpec().generate_genesis(
192-
genesis_time=Uint64(genesis_time),
193-
validators=make_validators(num_validators),
194-
)
195-
196-
197-
def reconstruct_block_from_header(state: State) -> Block:
198-
"""
199-
Rebuild the block matching a state's latest header.
200-
201-
The header pins the slot, proposer index, and parent root.
202-
The state root is the hash of the state itself.
203-
The body is the empty body of the genesis and empty-block convention.
204-
205-
For a genesis state this is the genesis block.
206-
"""
207-
return Block(
208-
slot=state.latest_block_header.slot,
209-
proposer_index=state.latest_block_header.proposer_index,
210-
parent_root=state.latest_block_header.parent_root,
211-
state_root=hash_tree_root(state),
212-
body=BlockBody(attestations=AggregatedAttestations(data=[])),
213-
)
214-
215-
216-
def make_genesis_store(
156+
def build_genesis_store(
217157
num_validators: int = 4,
218158
*,
219159
genesis_time: int = 0,
220-
validator_index: ValidatorIndex | None = _DEFAULT_VALIDATOR_INDEX,
160+
validator_index: ValidatorIndex | None = ValidatorIndex(0),
221161
observer: bool = False,
222162
keyed: bool = True,
223163
time: Interval | None = None,
224164
) -> Store:
225165
"""
226166
Build a genesis fork-choice store.
227167
228-
Uses real XMSS keys when keyed, else zeroed keys for any validator count.
229168
Set observer for a store with no owning validator.
230169
"""
231-
state = (
232-
generate_pre_state(genesis_time=Uint64(genesis_time), num_validators=num_validators)
233-
if keyed
234-
else make_genesis_state(num_validators=num_validators, genesis_time=genesis_time)
170+
# Slot 0 makes the anchor builder produce the genesis state and block pair.
171+
state, genesis_block = build_anchor(
172+
num_validators, Slot(0), genesis_time=Uint64(genesis_time), keyed=keyed
235173
)
236174
store = LstarSpec().create_store(
237175
state,
238-
reconstruct_block_from_header(state),
176+
genesis_block,
239177
validator_index=None if observer else validator_index,
240178
)
241179
return store if time is None else store.model_copy(update={"time": time})

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
from collections.abc import Callable
44
from typing import Any, ClassVar
55

6-
from consensus_testing.genesis import (
7-
build_anchor,
8-
generate_pre_state,
9-
reconstruct_block_from_header,
10-
)
6+
from consensus_testing.genesis import build_anchor
117
from consensus_testing.test_fixtures.base import BaseConsensusFixture, BaseTestSpec
128
from lean_spec.base import StrictBaseModel
139
from lean_spec.spec.forks import Slot
@@ -50,15 +46,8 @@ def _build_store(num_validators: int, genesis_time: int, anchor_slot: int = 0) -
5046
historical roots, and multi-node fork-choice trees.
5147
"""
5248
fork = LstarSpec()
53-
if anchor_slot == 0:
54-
state = generate_pre_state(
55-
fork=fork, genesis_time=Uint64(genesis_time), num_validators=num_validators
56-
)
57-
block = reconstruct_block_from_header(state)
58-
# No validator identity — fixture only reads store data, never signs.
59-
return fork.create_store(state, block, validator_index=None)
60-
6149
# Walk the chain from genesis through anchor_slot using empty blocks.
50+
# Slot 0 makes the builder return the genesis pair with no advance.
6251
# The returned pair (state, block) is internally consistent with the
6352
# historical chain the fixture wants to present to the endpoint.
6453
state, block = build_anchor(
@@ -67,6 +56,7 @@ def _build_store(num_validators: int, genesis_time: int, anchor_slot: int = 0) -
6756
anchor_slot=Slot(anchor_slot),
6857
genesis_time=Uint64(genesis_time),
6958
)
59+
# No validator identity — fixture only reads store data, never signs.
7060
return fork.create_store(state, block, validator_index=None)
7161

7262

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from pydantic import Field
88

9-
from consensus_testing.genesis import generate_pre_state, reconstruct_block_from_header
9+
from consensus_testing.genesis import build_genesis_state, reconstruct_block_from_header
1010
from consensus_testing.keys import XmssKeyManager
1111
from consensus_testing.test_fixtures.base import BaseConsensusFixture, BaseTestSpec
1212
from consensus_testing.test_types import (
@@ -85,7 +85,7 @@ class ForkChoiceTest(BaseTestSpec):
8585
description: ClassVar[str] = "Tests event-driven fork choice through Store operations"
8686
"""Human-readable summary for fixture documentation."""
8787

88-
anchor_state: State = Field(default_factory=generate_pre_state)
88+
anchor_state: State = Field(default_factory=build_genesis_state)
8989
"""
9090
Initial trusted consensus state.
9191
@@ -136,10 +136,7 @@ def _resolved_anchor_block(self) -> Block:
136136
"""
137137
if self.anchor_block is not None:
138138
return self.anchor_block
139-
# Build a minimal genesis block from the state's header fields.
140-
#
141-
# The state already contains the block header.
142-
# We extract its fields to create a matching Block.
139+
# The state already carries the block header, so rebuild the matching block.
143140
return reconstruct_block_from_header(self.anchor_state)
144141

145142
def _resolved_max_slot(self) -> Slot:

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pydantic import Field, model_validator
66

7-
from consensus_testing.genesis import generate_pre_state
7+
from consensus_testing.genesis import build_genesis_state
88
from consensus_testing.keys import XmssKeyManager
99
from consensus_testing.test_fixtures.base import BaseConsensusFixture, BaseTestSpec
1010
from consensus_testing.test_types import AggregatedAttestationSpec, BlockSpec, StateExpectation
@@ -74,7 +74,7 @@ class StateTransitionTest(BaseTestSpec):
7474
"epochs, and finality"
7575
)
7676

77-
pre: State = Field(default_factory=generate_pre_state)
77+
pre: State = Field(default_factory=build_genesis_state)
7878
"""
7979
The initial consensus state before processing.
8080

0 commit comments

Comments
 (0)