Skip to content

test(fc): safe target stays at justified root with insufficient weight #559

@tcoratger

Description

@tcoratger

Context

The safe target only advances when a block has at least 2/3 of total validator weight behind it. When attestation weight is insufficient, the safe target must remain at the justified root (typically genesis for a fresh chain).

This is the "negative" counterpart to the supermajority test — equally important for client teams to verify they don't incorrectly advance the safe target.

What to test

Write a fork choice filler that:

  1. Creates 6 validators (2/3 threshold = 4)
  2. Builds a chain
  3. Has only 2 out of 6 validators attest (below threshold)
  4. Ticks to interval 3 to trigger `update_safe_target`
  5. Verifies the safe target does NOT advance — it stays at the justified root

Key assertions

  • After interval 3 with only 2/6 attestations: safe target remains at genesis/justified root
  • `StoreChecks(safe_target=...)` validates the safe target is unchanged

Where to add the test

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

Code skeleton

def test_safe_target_stays_at_justified_with_insufficient_weight(
    fork_choice_test: ForkChoiceTestFiller,
) -> None:
    """Safe target does not advance when < 2/3 validators attest."""
    # 6 validators -> threshold = 4, only 2 attest
    fork_choice_test(
        num_validators=6,
        steps=[
            BlockStep(block=BlockSpec(slot=Slot(1), label="block_1")),
            BlockStep(block=BlockSpec(slot=Slot(2), label="block_2")),
            # Only 2 out of 6 validators attest (below threshold)
            AttestationStep(
                attestation=GossipAttestationSpec(
                    validator_id=ValidatorIndex(0),
                    slot=Slot(2),
                    target_slot=Slot(2),
                    target_root_label="block_2",
                ),
            ),
            AttestationStep(
                attestation=GossipAttestationSpec(
                    validator_id=ValidatorIndex(1),
                    slot=Slot(2),
                    target_slot=Slot(2),
                    target_root_label="block_2",
                ),
            ),
            # Tick to interval 3
            TickStep(
                time=...,  # interval 3 of slot 3
                checks=StoreChecks(
                    # safe_target should still be at genesis/justified root
                ),
            ),
        ],
    )

How to run

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

References

  • `Store.update_safe_target`: `src/lean_spec/subspecs/forkchoice/store.py`
  • Threshold arithmetic: `3 * count >= 2 * num_validators` — with 2/6: `32=6 < 26=12`, fails

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