fix(fork-choice): reject attestation slot before head block slot#442
fix(fork-choice): reject attestation slot before head block slot#442MegaRedHand wants to merge 2 commits into
Conversation
Port the head-consistency guard from leanSpec PR #1020. A gossip attestation's slot records when the head was observed, so it cannot precede the slot of the head block it claims to have seen. Without this lower bound the wire slot is untethered from any real block: a crafted near-`u64::MAX` slot probes the slot-to-interval multiplication in the time check. ethlambda already used `saturating_mul` there, so the overflow could not crash the node the way the unchecked Python constructor did; this adds the missing semantic guard and surfaces it as a new `AttestationSlotBeforeHead` rejection. Adds a negative vector for the before-head case and a ceiling-slot vector that confirms a near-`u64::MAX` slot is rejected cleanly as too-far-in-future. Claude-Session: https://claude.ai/code/session_018pvnQJkom3DobK9oSbK42T
🤖 Kimi Code ReviewThis is a well-crafted security fix. The change adds a critical consensus validation rule with proper defense-in-depth and test coverage. Security & Correctness
Code Quality
Testing
Minor Note
Verdict: No changes required. This is a correct, secure implementation of leanSpec #1020 with comprehensive test coverage. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewPR #442 Review:
|
🤖 Codex Code Review
I did not find another obvious problem in the diff itself. I wasn’t able to run the targeted Rust tests because Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryThis PR adds a semantic lower-bound guard to
Confidence Score: 4/5Safe to merge; the new check is a strict addition that only rejects previously-unchecked invalid attestations and does not affect any existing valid code path. The guard logic, error variant, and both unit tests are correct. The one issue is an inline comment that incorrectly attributes overflow protection to the new check when crates/blockchain/src/store.rs — specifically the inline comment on lines 228–229 of the head consistency check block.
|
| Filename | Overview |
|---|---|
| crates/blockchain/src/store.rs | Adds a head-consistency lower-bound check (data.slot < data.head.slot) to validate_attestation_data, a new StoreError::AttestationSlotBeforeHead variant, and two unit tests. Logic and tests are correct; one inline comment overstates the overflow-protection benefit of the new check. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[validate_attestation_data] --> B[Availability Check\nblocks exist in store]
B --> C[Topology Check\nsource ≤ target ≤ head slots]
C --> D[Consistency Check\ncheckpoint slots match block slots]
D --> E[Ancestry Check\nsource → target → head on one chain]
E --> F{data.slot < data.head.slot?}
F -- Yes --> G[❌ AttestationSlotBeforeHead]
F -- No --> H{attestation_start_interval >\nstore.time + GOSSIP_DISPARITY?}
H -- Yes --> I[❌ AttestationTooFarInFuture\nsaturating_mul prevents overflow]
H -- No --> J[✅ Ok]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[validate_attestation_data] --> B[Availability Check\nblocks exist in store]
B --> C[Topology Check\nsource ≤ target ≤ head slots]
C --> D[Consistency Check\ncheckpoint slots match block slots]
D --> E[Ancestry Check\nsource → target → head on one chain]
E --> F{data.slot < data.head.slot?}
F -- Yes --> G[❌ AttestationSlotBeforeHead]
F -- No --> H{attestation_start_interval >\nstore.time + GOSSIP_DISPARITY?}
H -- Yes --> I[❌ AttestationTooFarInFuture\nsaturating_mul prevents overflow]
H -- No --> J[✅ Ok]
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
crates/blockchain/src/store.rs:226-229
**Misleading overflow-protection rationale in comment**
The third sentence claims this check "keeps the wire slot clear of the overflow edge," implying it prevents a crafted near-`u64::MAX` slot from reaching the time check. That's not accurate: a gossip message can set `data.slot = u64::MAX` with a head block at any realistic slot (e.g. slot 2), which satisfies `data.slot >= data.head.slot` and passes straight through to `saturating_mul`. The second test in this PR (`validate_attestation_rejects_ceiling_slot_without_overflow`) explicitly demonstrates this path. The actual overflow protection comes entirely from the existing `saturating_mul`, not from this bound. The first two sentences are correct and self-contained; the third should be removed or rewritten to avoid misleading future readers into thinking this check provides overflow safety.
Reviews (1): Last reviewed commit: "fix(fork-choice): reject attestation slo..." | Re-trigger Greptile
What
Ports the head-consistency guard from leanSpec PR #1020 into
validate_attestation_data.A gossip attestation's slot records when the head was observed, so it cannot precede the slot of the head block it claims to have seen. We did not enforce this lower bound: the wire
slotfield was untethered from any real block, free to be set arbitrarily relative to the observed head.Why
2**64slot overflowed the slot→interval constructor and crashed the node (remote DoS).Note on the overflow half: ethlambda's time check already uses
saturating_mul(data.slot.saturating_mul(INTERVALS_PER_SLOT)), so a near-u64::MAXslot saturates tou64::MAXand is rejected cleanly as too-far-in-future rather than panicking. The Rust port was never crashable the way the unchecked Python constructor was, so no change to the time-check arithmetic is needed; the spec's switch to slot-unit comparison is mathematically equivalent to the existing interval comparison. This PR adds the genuinely missing piece: the semantic lower bound.Change
StoreError::AttestationSlotBeforeHead { attestation_slot, head_slot }rejection.data.slot < data.head.slotafter the ancestry checks, before the time check (mirrors the spec's ordering).AttestationSlotBeforeHead), and au64::MAXceiling slot (rejected asAttestationTooFarInFuture, confirming no overflow).Applies to both gossip paths via the shared
validate_attestation_data(on_gossip_attestationandon_gossip_aggregated_attestation).Testing
https://claude.ai/code/session_018pvnQJkom3DobK9oSbK42T