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:
- Builds a common prefix (block at slot 1)
- Produces two equivocating blocks at slot 2 (A and B)
- Validators 0 and 1 attest to fork A
- Validators 2 and 3 attest to fork B
- Verifies the head follows the fork with more weight (or tiebreaker if equal)
- Then: one more validator switches to fork B (now 3 vs 2)
- 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`
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:
Key assertions
Where to add the test
Add to: `tests/consensus/devnet/fc/test_equivocation.py`
Code skeleton
How to run
References