Skip to content

test(fc): equivocating proposer with attestations splitting across forks #562

@tcoratger

Description

@tcoratger

Context

This extends the basic equivocation test by adding attestations that split across both forks. When a proposer equivocates (produces two blocks at the same slot), honest validators may end up attesting to different forks — some see block A first, others see block B.

The fork choice must correctly track attestation weight on each fork and select the heavier one. If attestation weight shifts, the head should switch.

What to test

Write a fork choice filler that:

  1. Builds a common prefix (block at slot 1)
  2. Produces two equivocating blocks at slot 2 (A and B)
  3. Validators 0 and 1 attest to fork A
  4. Validators 2 and 3 attest to fork B
  5. Verifies the head follows the fork with more weight (or tiebreaker if equal)
  6. Then: one more validator switches to fork B (now 3 vs 2)
  7. Verifies the head flips to fork B

Key assertions

  • With 2 vs 2 attestations: head follows lexicographic tiebreaker
  • With 3 vs 2 attestations: head follows the heavier fork (fork B)
  • Both forks remain in the store (no pruning of the lighter fork)

Where to add the test

Add to: `tests/consensus/devnet/fc/test_equivocation.py`

Code skeleton

def test_equivocating_proposer_with_split_attestations(
    fork_choice_test: ForkChoiceTestFiller,
) -> None:
    """Attestations split across equivocating forks; head follows weight."""
    fork_choice_test(
        num_validators=6,
        steps=[
            BlockStep(block=BlockSpec(slot=Slot(1), label="block_1")),
            # Two equivocating blocks at slot 2
            BlockStep(
                block=BlockSpec(slot=Slot(2), parent_label="block_1", label="fork_a"),
            ),
            BlockStep(
                block=BlockSpec(slot=Slot(2), parent_label="block_1", label="fork_b"),
            ),
            # Validators 0,1 attest to fork_a
            AttestationStep(
                attestation=GossipAttestationSpec(
                    validator_id=ValidatorIndex(0),
                    slot=Slot(2),
                    target_slot=Slot(2),
                    target_root_label="fork_a",
                ),
            ),
            AttestationStep(
                attestation=GossipAttestationSpec(
                    validator_id=ValidatorIndex(1),
                    slot=Slot(2),
                    target_slot=Slot(2),
                    target_root_label="fork_a",
                ),
            ),
            # Validators 2,3,4 attest to fork_b (heavier)
            *[
                AttestationStep(
                    attestation=GossipAttestationSpec(
                        validator_id=ValidatorIndex(i),
                        slot=Slot(2),
                        target_slot=Slot(2),
                        target_root_label="fork_b",
                    ),
                )
                for i in range(2, 5)
            ],
            # After accepting attestations, head should be fork_b (3 > 2)
            # TODO: tick to interval 4 to accept attestations, then check head
        ],
    )

How to run

uv run fill --fork=devnet --clean -n auto -k test_equivocating_proposer_with_split

References

  • `Store._compute_lmd_ghost_head`: `src/lean_spec/subspecs/forkchoice/store.py`
  • Weight accumulation: `Store.compute_block_weights`
  • Existing reorg tests for attestation weight patterns: `tests/consensus/devnet/fc/test_fork_choice_reorgs.py`

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