Skip to content
Merged
21 changes: 8 additions & 13 deletions packages/testing/src/consensus_testing/genesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,12 @@
from lean_spec.forks.lstar.containers.state import State, Validators
from lean_spec.forks.lstar.containers.validator import Validator
from lean_spec.forks.lstar.spec import LstarSpec
from lean_spec.forks.protocol import ForkProtocol
from lean_spec.subspecs.ssz.hash import hash_tree_root
from lean_spec.types import Bytes52, Slot, Uint64, ValidatorIndex

from .keys import XmssKeyManager

_DEFAULT_GENESIS_TIME = Uint64(0)
_SPEC = LstarSpec()
"""Active fork spec — stateless, safe to share across all helper invocations."""

_DEFAULT_FORK: ForkProtocol = _SPEC
"""Stateless fork instance used when callers do not pass one explicitly."""


def _build_validators(num_validators: int) -> Validators:
Expand Down Expand Up @@ -44,30 +38,30 @@ def _build_validators(num_validators: int) -> Validators:


def generate_pre_state(
fork: ForkProtocol = _DEFAULT_FORK,
fork: LstarSpec | None = None,
genesis_time: Uint64 = _DEFAULT_GENESIS_TIME,
num_validators: int = 4,
) -> State:
"""Generate a default pre-state for consensus tests.

Args:
fork: Fork dispatching genesis construction.
fork: Fork dispatching genesis construction. Defaults to a fresh
LstarSpec instance.
genesis_time: The genesis timestamp.
num_validators: Number of validators to include.

Returns:
A properly initialized consensus state.
"""
fork = fork or LstarSpec()
validators = _build_validators(num_validators)
state = fork.generate_genesis(genesis_time=genesis_time, validators=validators)
assert isinstance(state, State)
return state
return fork.generate_genesis(genesis_time=genesis_time, validators=validators)


def build_anchor(
num_validators: int,
anchor_slot: Slot,
fork: ForkProtocol = _DEFAULT_FORK,
fork: LstarSpec | None = None,
genesis_time: Uint64 = _DEFAULT_GENESIS_TIME,
) -> tuple[State, Block]:
"""Build a consistent non-genesis anchor by advancing the genesis state.
Expand Down Expand Up @@ -101,6 +95,7 @@ def build_anchor(
"For a genesis anchor use generate_pre_state instead."
)

fork = fork or LstarSpec()
state = generate_pre_state(fork=fork, genesis_time=genesis_time, num_validators=num_validators)

# Reconstruct the genesis block from the state's latest header.
Expand All @@ -124,7 +119,7 @@ def build_anchor(
for next_slot in range(1, int(anchor_slot) + 1):
slot = Slot(next_slot)
proposer_index = ValidatorIndex(int(slot) % int(num_validators_u64))
current_block, state, _, _ = _SPEC.build_block(
current_block, state, _, _ = fork.build_block(
state,
slot=slot,
proposer_index=proposer_index,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _build_store(num_validators: int, genesis_time: int, anchor_slot: int = 0) -
)
block = _make_genesis_block(state)
# No validator identity — fixture only reads store data, never signs.
return Store.from_anchor(state, block, validator_id=None)
return fork.create_store(state, block, validator_id=None)

# Walk the chain from genesis through anchor_slot using empty blocks.
# The returned pair (state, block) is internally consistent with the
Expand All @@ -64,7 +64,7 @@ def _build_store(num_validators: int, genesis_time: int, anchor_slot: int = 0) -
anchor_slot=Slot(anchor_slot),
genesis_time=Uint64(genesis_time),
)
return Store.from_anchor(state, block, validator_id=None)
return fork.create_store(state, block, validator_id=None)


def _health_response(_store: Store, _fixture: "ApiEndpointTest") -> dict[str, Any]:
Expand Down Expand Up @@ -100,7 +100,7 @@ def _finalized_state_response(store: Store, _fixture: "ApiEndpointTest") -> dict

def _fork_choice_response(store: Store, _fixture: "ApiEndpointTest") -> dict[str, Any]:
"""Fork choice tree: blocks with weights, head, checkpoints, validator count."""
weights = store.compute_block_weights()
weights = LstarSpec().compute_block_weights(store)

# Only post-finalization blocks are relevant to head selection.
nodes = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from pydantic import Field, model_validator

from lean_spec.forks.lstar import Store
from lean_spec.forks.lstar.containers.block import (
Block,
BlockBody,
Expand All @@ -38,9 +37,6 @@
)
from .base import BaseConsensusFixture

_SPEC = LstarSpec()
"""Active fork spec — stateless, safe to share across all fixture invocations."""


class ForkChoiceTest(BaseConsensusFixture):
"""
Expand Down Expand Up @@ -190,6 +186,8 @@ def make_fixture(self) -> Self:
assert self.anchor_block is not None, "anchor block must be set before making fixture"
assert self.max_slot is not None, "max slot must be set before making fixture"

spec = LstarSpec()

# Expected anchor-init failure path.
#
# When anchor_valid is False, the test asserts that Store.from_anchor
Expand All @@ -202,7 +200,7 @@ def make_fixture(self) -> Self:
"Store.from_anchor is expected to fail before any step can run"
)
try:
Store.from_anchor(
spec.create_store(
self.anchor_state,
self.anchor_block,
validator_id=ValidatorIndex(0),
Expand Down Expand Up @@ -257,7 +255,7 @@ def make_fixture(self) -> Self:
#
# The Store is the node's local view of the chain.
# It starts from a trusted anchor (usually genesis).
store = Store.from_anchor(
store = spec.create_store(
self.anchor_state,
self.anchor_block,
validator_id=ValidatorIndex(0),
Expand Down Expand Up @@ -293,7 +291,7 @@ def make_fixture(self) -> Self:
target_interval = Interval.from_unix_time(
Uint64(step.time), store.config.genesis_time
)
store, _ = _SPEC.on_tick(
store, _ = spec.on_tick(
store,
target_interval,
has_proposal=step.has_proposal,
Expand Down Expand Up @@ -326,13 +324,13 @@ def make_fixture(self) -> Self:
# This tick includes a block (has proposal).
# Always act as aggregator to ensure gossip signatures are aggregated
target_interval = Interval.from_slot(block.slot)
store, _ = _SPEC.on_tick(
store, _ = spec.on_tick(
store, target_interval, has_proposal=True, is_aggregator=True
)

# Process the block through Store.
# This validates, applies state transition, and updates the store's head.
store = _SPEC.on_block(
store = spec.on_block(
store,
signed_block,
scheme=LEAN_ENV_TO_SCHEMES[self.lean_env],
Expand All @@ -350,7 +348,7 @@ def make_fixture(self) -> Self:
step.valid,
)
step._filled_attestation = signed_attestation
store = _SPEC.on_gossip_attestation(
store = spec.on_gossip_attestation(
store,
signed_attestation,
scheme=LEAN_ENV_TO_SCHEMES[self.lean_env],
Expand All @@ -364,7 +362,7 @@ def make_fixture(self) -> Self:
key_manager,
)
step._filled_attestation = signed_aggregated
store = _SPEC.on_gossip_aggregated_attestation(store, signed_aggregated)
store = spec.on_gossip_aggregated_attestation(store, signed_aggregated)

case _:
raise ValueError(f"Step {i}: unknown step type {type(step).__name__}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
from ..test_types import AggregatedAttestationSpec, BlockSpec, StateExpectation
from .base import BaseConsensusFixture

_SPEC = LstarSpec()
"""Active fork spec — stateless, safe to share across all fixture invocations."""


class StateTransitionTest(BaseConsensusFixture):
"""
Expand Down Expand Up @@ -113,6 +110,7 @@ def make_fixture(self) -> "StateTransitionTest":
"""
actual_post_state: State | None = None
exception_raised: Exception | None = None
spec = LstarSpec()

# Initialize filled_blocks list that will be populated as we process blocks
filled_blocks: list[Block] = []
Expand Down Expand Up @@ -140,9 +138,9 @@ def make_fixture(self) -> "StateTransitionTest":
if cached_state is not None:
state = cached_state
elif getattr(block_spec, "skip_slot_processing", False):
state = _SPEC.process_block(state, block)
state = spec.process_block(state, block)
else:
state = _SPEC.state_transition(
state = spec.state_transition(
state,
block=block,
valid_signatures=True,
Expand Down Expand Up @@ -217,7 +215,7 @@ def _build_block_from_spec(
# Advance slots unless the spec intentionally skips slot processing.
slot_advanced_state: State | None = None
if not spec.skip_slot_processing:
slot_advanced_state = _SPEC.process_slots(state, spec.slot)
slot_advanced_state = LstarSpec().process_slots(state, spec.slot)

# Resolve the parent root.
# Default: latest block header from the slot-advanced state.
Expand Down Expand Up @@ -260,7 +258,7 @@ def _build_block_from_spec(

known_block_roots = frozenset(hash_tree_root(b) for b in block_registry.values())

block, post_state, _, _ = _SPEC.build_block(
block, post_state, _, _ = LstarSpec().build_block(
state,
slot=spec.slot,
proposer_index=proposer_index,
Expand Down Expand Up @@ -295,8 +293,8 @@ def _build_block_from_spec(
# The body changed, so re-run the transition to get the correct
# post-state and state root.
if post_state is not None:
post_state = _SPEC.process_slots(state, spec.slot)
post_state = _SPEC.process_block(post_state, block)
post_state = LstarSpec().process_slots(state, spec.slot)
post_state = LstarSpec().process_block(post_state, block)
block = block.model_copy(update={"state_root": hash_tree_root(post_state)})

return block, post_state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
from ..test_types import BlockSpec
from .base import BaseConsensusFixture

_SPEC = LstarSpec()
"""Active fork spec — stateless, safe to share across all fixture invocations."""


class VerifySignaturesTest(BaseConsensusFixture):
"""
Expand Down Expand Up @@ -115,7 +112,7 @@ def make_fixture(self) -> VerifySignaturesTest:

# Verify signatures
try:
_SPEC.verify_signatures(signed_block, self.anchor_state.validators)
LstarSpec().verify_signatures(signed_block, self.anchor_state.validators)
except AssertionError as e:
exception_raised = e
# If we expect an exception, this is fine
Expand Down
31 changes: 15 additions & 16 deletions packages/testing/src/consensus_testing/test_types/block_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@
from ..keys import LEAN_ENV_TO_SCHEMES, XmssKeyManager, create_dummy_signature
from .aggregated_attestation_spec import AggregatedAttestationSpec

_SPEC = LstarSpec()
"""Active fork spec — stateless, safe to share across all spec invocations."""


class BlockSpec(CamelModel):
"""
Expand Down Expand Up @@ -294,6 +291,7 @@ def build_signed_block(
Returns:
Complete signed block with all attestation and proposer signatures.
"""
spec = LstarSpec()
proposer_index = self.resolve_proposer_index(len(state.validators))

# Build a genesis block registry so attestation specs can resolve labels.
Expand All @@ -308,7 +306,7 @@ def build_signed_block(

# Resolve the parent root.
# The default is the latest block header from the slot-advanced state.
parent_state = _SPEC.process_slots(state, self.slot)
parent_state = spec.process_slots(state, self.slot)
parent_root = self.resolve_parent_root(
block_registry,
default_root=hash_tree_root(parent_state.latest_block_header),
Expand Down Expand Up @@ -364,7 +362,7 @@ def build_signed_block(
for agg_att, proof in zip(aggregated_attestations, attestation_sigs.data, strict=True)
}

final_block, _, _, aggregated_signatures = _SPEC.build_block(
final_block, _, _, aggregated_signatures = spec.build_block(
state,
slot=self.slot,
proposer_index=proposer_index,
Expand Down Expand Up @@ -405,6 +403,7 @@ def build_signed_block_with_store(
Returns:
Complete signed block ready for Store processing.
"""
spec = LstarSpec()
proposer_index = self.resolve_proposer_index(len(store.states[store.head].validators))

# Resolve parent block.
Expand All @@ -429,7 +428,7 @@ def build_signed_block_with_store(
# check rejects votes whose slot has not yet started locally.
block_slot_interval = Interval.from_slot(self.slot)
if store.time < block_slot_interval:
store, _ = _SPEC.on_tick(
store, _ = spec.on_tick(
store, block_slot_interval, has_proposal=True, is_aggregator=True
)

Expand All @@ -442,7 +441,7 @@ def build_signed_block_with_store(
or (signature := sigs_for_data.get(attestation.validator_id)) is None
):
continue
store = _SPEC.on_gossip_attestation(
store = spec.on_gossip_attestation(
store,
SignedAttestation(
validator_id=attestation.validator_id,
Expand All @@ -454,11 +453,11 @@ def build_signed_block_with_store(
)

# Trigger Store aggregation to merge gossip signatures into known payloads.
aggregation_store, _ = store.aggregate()
merged_store = aggregation_store.accept_new_attestations()
aggregation_store, _ = spec.aggregate(store)
merged_store = spec.accept_new_attestations(aggregation_store)

# Build the block through the spec's State.build_block().
final_block, _, _, block_proofs = _SPEC.build_block(
final_block, _, _, block_proofs = spec.build_block(
parent_state,
slot=self.slot,
proposer_index=proposer_index,
Expand All @@ -470,9 +469,9 @@ def build_signed_block_with_store(
# Append forced attestations that bypass the builder's MAX cap.
# Each entry is signed and aggregated so the block carries valid proofs.
if self.forced_attestations:
for spec in self.forced_attestations:
att_data = spec.build_attestation_data(block_registry, parent_state)
proof = key_manager.sign_and_aggregate(spec.validator_ids, att_data)
for att_spec in self.forced_attestations:
att_data = att_spec.build_attestation_data(block_registry, parent_state)
proof = key_manager.sign_and_aggregate(att_spec.validator_ids, att_data)
block_proofs.append(proof)
final_block = final_block.model_copy(
update={
Expand All @@ -483,7 +482,7 @@ def build_signed_block_with_store(
*final_block.body.attestations.data,
AggregatedAttestation(
aggregation_bits=ValidatorIndices(
data=spec.validator_ids,
data=att_spec.validator_ids,
).to_aggregation_bits(),
data=att_data,
),
Expand All @@ -495,8 +494,8 @@ def build_signed_block_with_store(
)

# Recompute state root with the modified body.
post_state = _SPEC.process_slots(parent_state, self.slot)
post_state = _SPEC.process_block(post_state, final_block)
post_state = spec.process_slots(parent_state, self.slot)
post_state = spec.process_block(post_state, final_block)
final_block = final_block.model_copy(update={"state_root": hash_tree_root(post_state)})

return self._sign_block(final_block, block_proofs, proposer_index, key_manager)
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
from ..keys import XmssKeyManager, create_dummy_signature
from .utils import resolve_checkpoint

_SPEC = LstarSpec()
"""Active fork spec — stateless, safe to share across all spec invocations."""


class GossipAttestationSpec(CamelModel):
"""
Expand Down Expand Up @@ -204,7 +201,7 @@ def build_signed(
attestation_data = self.build_attestation_data(block_registry, anchor_block)
else:
# Honest path: use the Store's own attestation data production.
attestation_data = _SPEC.produce_attestation_data(store, self.slot)
attestation_data = LstarSpec().produce_attestation_data(store, self.slot)

signature = (
key_manager.sign_attestation_data(self.validator_id, attestation_data)
Expand Down
Loading
Loading