Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ class StateTransitionFixture(BaseConsensusFixture):
post_state_root: Bytes32 | None = None
"""Hash tree root of the full post-state, so two clients cannot pass with divergent state."""

skip_slot_processing: list[bool] | None = None
"""
Per block, whether the runner must skip slot processing before processing that block.

Most tests advance slots for every block, and there the field is absent. It is emitted
only by the tests that deliberately leave the state behind the block's slot, because
that is the state they are testing the rejection of. A runner that calls the whole state
transition for such a block advances the slot first and never reaches the mismatch, so
without this the fixture cannot be reproduced from its own contents.
"""


class StateTransitionTest(BaseTestSpec):
"""
Expand Down Expand Up @@ -140,12 +151,18 @@ def generate(self) -> StateTransitionFixture:
if self.post is not None and actual_post_state is not None:
self.post.validate_against_state(actual_post_state, block_registry=block_registry)

# Emitted only when some block needs it, so the ordinary fixture is unchanged.
skip_slot_processing = [block_spec.skip_slot_processing for block_spec in self.blocks]
if not any(skip_slot_processing):
skip_slot_processing = None

return StateTransitionFixture(
pre=self.pre,
blocks=filled_blocks,
post=self.post,
post_state_root=post_state_root,
rejection_reason=rejection_reason,
skip_slot_processing=skip_slot_processing,
)

def _build_block_from_spec(
Expand Down
62 changes: 62 additions & 0 deletions tests/testing/test_state_transition_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""The emitted state-transition fixture must be reproducible from its own contents."""

from __future__ import annotations

from consensus_testing import BlockSpec, ExpectedRejection, build_genesis_state
from consensus_testing.test_fixtures.state_transition import StateTransitionTest
from lean_spec.spec.forks import RejectionReason, Slot
from lean_spec.spec.forks.lstar.spec import LstarSpec


def test_an_ordinary_fixture_carries_no_slot_processing_instruction() -> None:
"""A test that advances slots normally emits nothing extra, as every runner does that."""
fixture = StateTransitionTest(
pre=build_genesis_state(),
blocks=[BlockSpec(slot=Slot(1)), BlockSpec(slot=Slot(2))],
).generate()

assert fixture.skip_slot_processing is None


def test_a_skipped_slot_processing_block_says_so_in_the_fixture() -> None:
"""
A block the spec processes without advancing slots must say so.

The rejection under test only happens while the state is behind the block's slot. A
runner that calls the whole state transition advances the slot first, never reaches the
mismatch, and reports the fixture as broken rather than reproducing it.
"""
pre = LstarSpec().process_slots(build_genesis_state(), Slot(1))

fixture = StateTransitionTest(
pre=pre,
blocks=[BlockSpec(slot=Slot(2), skip_slot_processing=True)],
expected_rejection=ExpectedRejection(
reason=RejectionReason.BLOCK_SLOT_MISMATCH,
exact_message="Block slot mismatch",
),
).generate()

assert fixture.skip_slot_processing == [True]


def test_the_instruction_is_per_block_and_in_order() -> None:
"""
A run that mixes both kinds pins which block skips, not merely that one of them does.

A single flag, or a list built in the wrong order, would pass a test where every block
skips. This one would not.
"""
pre = LstarSpec().process_slots(build_genesis_state(), Slot(1))

fixture = StateTransitionTest(
pre=pre,
blocks=[
# Processed against a state already at slot 1, so slots must not advance again.
BlockSpec(slot=Slot(1), skip_slot_processing=True),
# An ordinary block, which the runner advances to as usual.
BlockSpec(slot=Slot(2)),
],
).generate()

assert fixture.skip_slot_processing == [True, False]