Skip to content

test(st): attestation with unjustified source is silently skipped #575

@tcoratger

Description

@tcoratger

Context

During block processing, `State.process_attestations` validates each attestation before applying it. One critical check: the attestation's source checkpoint slot must be justified in the current state.

If the source slot is NOT justified, the attestation is silently skipped — no error is raised, no state change occurs. This is by design: the attestation is simply ignored.

The check in `process_attestations`:
```python
if not self.justified_slots.is_slot_justified(att_data.source.slot, ...):
continue # Skip unjustified source
```

This is important because:

  • Validators may reference checkpoints that the proposer hasn't seen justify yet
  • Blocks should not be rejected for containing such attestations
  • The attestation is simply ineffective, not invalid

No spec test filler specifically tests the unjustified-source skip path.

What to test

Write a state transition filler that:

  1. Creates a chain where slot X is NOT justified
  2. Includes an attestation in a block with `source.slot = X`
  3. Verifies the attestation is silently skipped (no justification change, no error)
  4. Includes another attestation with a valid (justified) source in the same block
  5. Verifies that valid attestation IS processed

Key assertions

  • Post-state shows no justification from the unjustified-source attestation
  • Post-state DOES show effects from the valid attestation
  • No error during block processing
  • `StateExpectation` validates that only the valid attestation's effects are visible

Where to add the test

Add to: `tests/consensus/devnet/state_transition/test_justification.py`

Code skeleton

def test_attestation_with_unjustified_source_is_silently_skipped(
    state_transition_test: StateTransitionTestFiller,
) -> None:
    """Attestation whose source slot is not justified is ignored without error."""
    # Setup:
    # 1. Build chain to slot 5 (no justification yet beyond genesis)
    # 2. Create attestation A with source=slot 3 (not justified) -> should be skipped
    # 3. Create attestation B with source=slot 0 (genesis, justified) -> should work
    # 4. Include both in a block
    # 5. Verify only B's effects appear in post-state
    state_transition_test(
        pre=generate_pre_state(num_validators=4, genesis_time=Uint64(0)),
        blocks=[
            BlockSpec(slot=Slot(1)),
            BlockSpec(slot=Slot(2)),
            BlockSpec(
                slot=Slot(3),
                attestations=[
                    # Attestation with unjustified source (slot 2 is not justified)
                    # This should be silently skipped
                    AggregatedAttestationSpec(
                        validator_ids=[ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)],
                        slot=Slot(3),
                        target_slot=Slot(2),  # Target
                        target_root_label="...",
                        # source_slot would need to reference an unjustified slot
                        # TODO: verify how to set source explicitly in AggregatedAttestationSpec
                    ),
                ],
            ),
        ],
        post=StateExpectation(
            slot=Slot(3),
            # No justification change from the skipped attestation
        ),
    )

How to run

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

References

  • `State.process_attestations`: `src/lean_spec/subspecs/containers/state/state.py`
  • `JustifiedSlots.is_slot_justified`: `src/lean_spec/subspecs/containers/state/types.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