Skip to content

test(fc): tick interval 0 skips attestation acceptance when not proposer #557

@tcoratger

Description

@tcoratger

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:

  1. Builds a short chain with 4 validators
  2. Gossips attestations from multiple validators
  3. At a slot where the local validator is NOT the proposer: ticks to interval 0 and verifies attestations are NOT yet accepted
  4. 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

Metadata

Metadata

Assignees

Labels

good first issueGood for newcomerstestsScope: Changes to the spec tests

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions