fix(state-transition): reject malformed justification state with typed errors (leanSpec #1178)#502
Conversation
…h typed errors Port leanSpec #1178. A State reconstructed from untrusted bytes (checkpoint sync) can satisfy SSZ yet still violate the cross-field invariant that the flat justification vote list is exactly one validator-sized segment per tracked root. process_attestations unpacked it with a bare index range, silently producing short segments for a malformed state. Guard before the unpack: - reject an empty validator registry (NoValidators), - reject a vote-list length != tracked-root count * validator count (new JustificationVotesLengthMismatch). Checks now fire in spec order (registry -> length -> zero-hash). The zero-hash guard and the totality of slot_is_justifiable_after were already present.
🤖 Kimi Code ReviewThis is a well-crafted defensive programming PR that closes an invariant violation vector reachable via malicious checkpoint-sync anchors. The validation logic is sound and the test coverage is comprehensive. Security & Correctness
let expected_vote_count = state
.justifications_roots
.len()
.checked_mul(validator_count)
.ok_or(Error::ArithmeticOverflow)?; // or expect() with a safety comment
Code Quality
Nitpicks
Verdict: Approved with minor suggestions. The overflow check is optional given SSZ bounds, but recommended for cryptographic code. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
Otherwise the patch itself looks sensible: the new typed error is clearer than silently fabricating short vote segments, and the added tests cover the intended failure modes well. I couldn’t run the touched tests here because Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview: PR 502 — reject malformed justification bookkeeping with typed errorsSummary: Clean, well-scoped defensive fix. Adds two guards in Correctness
Tests
Style / consistency
No issues found — this is a solid, minimal, and correctly justified hardening fix. Automated review by Claude (Anthropic) · sonnet · custom prompt |
Greptile SummaryPorts the reachable robustness guards from leanSpec #1178 into
Confidence Score: 4/5The guards and new error variant are correct; for any well-formed state none of the new checks fire, so normal operation is unaffected. Safe to merge after addressing the optional wording note. The validation logic is sound, the checks are placed in spec-prescribed order, both new error paths have dedicated tests, and the moved validator_count binding is used consistently throughout the function. The only note is a minor grammatical ambiguity in the JustificationVotesLengthMismatch error format string where the {expected} interpolation reads as if it qualifies validator count rather than the computed product. Only crates/blockchain/state_transition/src/lib.rs changed; the error message wording is worth a second look but nothing else requires attention.
|
| Filename | Overview |
|---|---|
| crates/blockchain/state_transition/src/lib.rs | Adds two pre-flight guards to process_attestations (empty validator registry and flat-vote-list length mismatch) with a new typed error variant, plus matching tests; logic is correct, minor wording ambiguity in the error format string. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[process_attestations called] --> B{validator_count == 0?}
B -- Yes --> C[Err: NoValidators]
B -- No --> D{vote list len mismatch?}
D -- Yes --> E[Err: JustificationVotesLengthMismatch]
D -- No --> F{any root is zero hash?}
F -- Yes --> G[Err: ZeroHashInJustificationRoots]
F -- No --> H[Unpack flat vote list into HashMap]
H --> I[Process attestations loop]
I --> J[Update justification and finalization state]
J --> K[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[process_attestations called] --> B{validator_count == 0?}
B -- Yes --> C[Err: NoValidators]
B -- No --> D{vote list len mismatch?}
D -- Yes --> E[Err: JustificationVotesLengthMismatch]
D -- No --> F{any root is zero hash?}
F -- Yes --> G[Err: ZeroHashInJustificationRoots]
F -- No --> H[Unpack flat vote list into HashMap]
H --> I[Process attestations loop]
I --> J[Update justification and finalization state]
J --> K[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/state_transition/src/lib.rs:40-42
The `{expected}` placeholder is positioned after "validator count" in the error string, so it reads as though `{expected}` *is* the validator count, when it is actually the product `roots.len() * validator_count`. For example, with 3 roots and 4 validators the message emits "…validator count 12", which could mislead a reader into thinking the validator set has 12 members. Breaking the sentence so that `{expected}` stands alone as the computed total removes the ambiguity.
```suggestion
#[error(
"justification vote list length {actual} does not equal expected {expected} (= tracked-root count × validator count)"
)]
```
Reviews (1): Last reviewed commit: "fix(state-transition): reject malformed ..." | Re-trigger Greptile
What
Ports the reachable robustness guards from leanSpec #1178: reject a malformed justification-bookkeeping
Stateinprocess_attestationswith typed errors instead of silently mis-counting.Why
process_attestationsunpacks the flatjustifications_validatorsbit list as one validator-sized segment per tracked root. AStatereconstructed from untrusted bytes (checkpoint sync) satisfies SSZ yet can still break the cross-field invariantlen(justifications_validators) == len(justifications_roots) * validator_count— SSZ decoding cannot enforce it. ethlambda's index-range unpack (.get(j)) does not crash on a mismatch, but it silently produces short/wrong vote segments. These guards close that gap.Changes
crates/blockchain/state_transition/src/lib.rs, inprocess_attestations, before the unpack:validator_count == 0(NoValidators). Belt-and-suspenders — the header stage rejects this first in the normal flow — but the unpack relies on a non-zero segment width directly.justifications_validators.len() != justifications_roots.len() * validator_count(newJustificationVotesLengthMismatch).Checks are ordered to match the spec (registry → length → zero-hash). The zero-hash-root guard and the totality of
slot_is_justifiable_after(the other two items in #1178) were already present in ethlambda, so they are unchanged.For a well-formed state none of these fire, so honest operation and state roots are unaffected.
Tests
process_attestations_rejects_justification_votes_length_mismatchprocess_attestations_rejects_empty_validator_registrycargo fmt,clippy -D warnings, and the state-transition lib tests pass.