Skip to content

produce_attestation_data asserts source <= target, but the target walk can land behind the head's justified checkpoint #1206

Description

@adust09

What

produce_attestation_data asserts justified_source.slot <= target_checkpoint.slot. The assert is reachable from a store the fork-choice rules themselves produce, so the reference validator can crash on the attestation duty, and a client that transcribes the assert as a rejection skips the vote.

How it is reached

get_attestation_target bounds its first walk by max(safe_target, finalized) and its second walk (justifiability) by finalized only. Nothing bounds either walk by the head's justified checkpoint, which is what the source is taken from.

Observed on a three-node devnet (4 s slots, one proposer per slot, many orphans). The persisted head chain was

62 -> 59 -> 51 -> 50 -> 49 -> 45 -> 42

with latest_finalized at 42 and the head state's latest_justified at 49. The lookback walk from 62 takes three steps to 50. Neither 50 nor 49 is justifiable after 42 (deltas 8 and 7), so the justifiability walk continues to 45, which is behind the source at 49. The assert fires.

Any sparse chain where JUSTIFICATION_LOOKBACK_SLOTS steps span more than five slots past the justified checkpoint can do this; the gap between 45 and 49 comes from the justifiability rule, not from the walk length.

What clients do

Every client I checked already deviates from the spec here, in the same direction:

client where shape
ethlambda crates/blockchain/src/store.rs, get_attestation_target_with_checkpoints after the walk, target below justified → target = justified ("not in the spec")
ream crates/common/fork_choice/lean/src/store.rs same
zeam pkgs/node/src/forkchoice.zig, getAttestationTargetUnlocked same, returns latest_justified
gean internal/attestation/produce.go after GetAttestationTarget, source.Slot > target.Slot → target = source
lantern src/consensus/state.c, lantern_state_compute_vote_checkpoints same
qlean-mini src/blockchain/fork_choice.cpp, getAttestationTarget first walk bounded by max(safe_target, justified); the second walk is unbounded, so the case above still reaches it

So five of six clients vote for the justified checkpoint when the walk lands behind it, and none of them can be matched against the spec as written.

Proposal

Make the spec say what the clients do: in produce_attestation_data, when the source is after the target, vote for the justified checkpoint as the target. It is on the head chain, it is justifiable by construction, and the vote keeps its weight in fork choice instead of being dropped.

Keeping the clamp in produce_attestation_data rather than in get_attestation_target leaves the target walk, and the fixtures that check it through attestation_target_slot, unchanged.

PR to follow.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions