Context
At interval 0 of each slot, the fork choice store conditionally accepts new attestations — but only if the local validator is the proposer for that slot. If the local validator is not the proposer, interval 0 is a no-op.
This distinction matters because:
- Proposer at interval 0: accepts attestations early to include them in the block
- Non-proposer at interval 0: waits until interval 4 (unconditional acceptance)
The proposer selection is round-robin: `proposer_index = slot % num_validators`.
No spec test filler currently verifies this conditional behavior.
What to test
Write a fork choice filler that:
- Builds a short chain with 4 validators
- Gossips attestations from multiple validators
- At a slot where the local validator is NOT the proposer: ticks to interval 0 and verifies attestations are NOT yet accepted
- At a slot where the local validator IS the proposer: ticks to interval 0 and verifies attestations ARE accepted
Key assertions
- After interval 0 (non-proposer slot): head unchanged (attestations not yet incorporated)
- After interval 0 (proposer slot): head may update (attestations accepted)
- After interval 4 (any slot): attestations always accepted
Where to add the test
Add to: tests/consensus/devnet/fc/test_tick_system.py
Code skeleton
def test_tick_interval_0_skips_acceptance_when_not_proposer(
fork_choice_test: ForkChoiceTestFiller,
) -> None:
"""Interval 0 only accepts attestations when the local validator is the proposer."""
# With 4 validators and round-robin: slot 0 -> validator 0, slot 1 -> validator 1, etc.
# Choose a slot where the test's local validator is NOT the proposer.
fork_choice_test(
steps=[
BlockStep(block=BlockSpec(slot=Slot(1), label="block_1")),
BlockStep(block=BlockSpec(slot=Slot(2), label="block_2")),
# Gossip attestations
AttestationStep(
attestation=GossipAttestationSpec(
validator_id=ValidatorIndex(1),
slot=Slot(2),
target_slot=Slot(2),
target_root_label="block_2",
),
),
# Tick to interval 0 of a non-proposer slot
# Verify attestations are NOT accepted yet
TickStep(
time=..., # interval 0 of non-proposer slot
checks=StoreChecks(head_slot=Slot(2)),
),
# Tick to interval 4 of the same slot
# Verify attestations ARE now accepted
TickStep(
time=..., # interval 4 of same slot
checks=StoreChecks(head_slot=Slot(2)),
),
],
)
How to run
uv run fill --fork=devnet --clean -n auto -k test_tick_interval_0_skips
References
Store.tick_interval: src/lean_spec/subspecs/forkchoice/store.py — see interval 0 branch
Validator.is_proposer_for: src/lean_spec/subspecs/containers/validator.py
- Round-robin:
slot % num_validators == validator_index
Context
At interval 0 of each slot, the fork choice store conditionally accepts new attestations — but only if the local validator is the proposer for that slot. If the local validator is not the proposer, interval 0 is a no-op.
This distinction matters because:
The proposer selection is round-robin: `proposer_index = slot % num_validators`.
No spec test filler currently verifies this conditional behavior.
What to test
Write a fork choice filler that:
Key assertions
Where to add the test
Add to:
tests/consensus/devnet/fc/test_tick_system.pyCode skeleton
How to run
References
Store.tick_interval:src/lean_spec/subspecs/forkchoice/store.py— see interval 0 branchValidator.is_proposer_for:src/lean_spec/subspecs/containers/validator.pyslot % num_validators == validator_index