fix: spec-faithful aggregation_bits bounds handling in process_attestations#425
Conversation
A skipped oversized attestation still inserted an all-false votes entry into `justifications` (and bumped attestations_processed), so the spurious root was serialized into the post-state. Move the bounds check above the insert so a skipped attestation leaves no trace.
🤖 Kimi Code ReviewOverall Assessment: Correct and necessary consensus-layer fix. The change from skipping invalid attestations to rejecting the block prevents consensus splits with other clients (Zeam, Lantern) and aligns with the spec's crash behavior (IndexError/AssertionError). Specific Feedback: 1. Performance (Minor)
// Single-pass alternative (optional optimization)
let mut participant_count = 0;
let oob_bit = attestation.aggregation_bits.iter().enumerate().find(|&(i, b)| {
if b {
participant_count += 1;
i >= validator_count
} else {
false
}
});
if participant_count == 0 { return Err(Error::EmptyAggregationBits); }
if let Some((index, _)) = oob_bit { /* ... */ }2. Unused Import Warning
3. Test Coverage
4. Code Clarity
Security/Consensus: Item 1 (two-pass iteration) is a performance note, not a security issue. The consensus logic is correct—returning Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code ReviewNo findings. The change in crates/blockchain/state_transition/src/lib.rs looks like a consensus-safety improvement: malformed aggregated attestations now hard-fail the block instead of being silently skipped, while still allowing oversized bitlists when every set bit is within the validator set, which matches the intended spec behavior described in the comments. The added coverage in crates/blockchain/state_transition/src/lib.rs exercises the important edge cases. Residual risk: I could not run the targeted tests in this environment. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryThis PR fixes audit finding C1 by moving the
Confidence Score: 5/5Safe to merge; the fix correctly eliminates the spurious justification entry and aligns block-rejection semantics with leanSpec and peer implementations. The two new checks fire before any state is touched, the store wraps the STF in a copy-on-write pattern so partial mutations on error are safely discarded, the spec table in the PR description is accurately reflected in both the implementation and the new unit test, and no unintended regressions were identified. No files require special attention.
|
| Filename | Overview |
|---|---|
| crates/blockchain/state_transition/src/lib.rs | Moves OOB/empty checks before entry() insert, changes skip→reject semantics with two new error variants, and adds a comprehensive unit test covering all three spec-aligned cases; logic is correct and state handling on error is safe. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[for attestation in attestations] --> B{is_valid_vote?}
B -- no --> A
B -- yes --> C{count_ones == 0?}
C -- yes --> ERR1[Err: EmptyAggregationBits\nblock rejected]
C -- no --> D{any set bit at index\n>= validator_count?}
D -- yes --> ERR2[Err: AggregationBitsOutOfBounds\nblock rejected]
D -- no --> E[attestations_processed += 1\njustifications.entry insert\nvote bits marked]
E --> F{supermajority?}
F -- yes --> G[update latest_justified\nset justified_slots\ntry_finalize]
G --> A
F -- no --> A
A -- done --> H[serialize_justifications\nmetrics update\nOk]
Reviews (1): Last reviewed commit: "fix: reject blocks with out-of-bounds or..." | Re-trigger Greptile
🤖 Claude Code ReviewI have enough context to write a thorough review. Review: fix: spec-faithful aggregation_bits bounds handling in process_attestationsSummaryThis PR fixes a genuine spec-divergence bug (audit finding C1) and deserves approval. The core logic is correct, the test coverage is solid, and the reasoning matches both the spec and peer implementations (Zeam, Lantern). A few observations below. CorrectnessThe main fix is correct. Moving the bounds checks above the Behavioral change for "oversized, all in-range" case is spec-faithful. The old OOB detection is precise: let oob_bit = (validator_count..attestation.aggregation_bits.len())
.find(|&i| attestation.aggregation_bits.get(i) == Some(true));The range Vote-loop safety after OOB check passes ( Potential IssuesMinor:
.take(attestation.aggregation_bits.len())After the OOB check guarantees all set bits are in Minor: Empty-bits check fires before OOB check for An attestation with zero set bits and an oversized bitlist returns Test CoverageThe three-case test
One gap worth considering: no test covers the happy path where Error MessagesBoth error variants have clear, informative messages: The OverallThe fix is correct, spec-faithful, and well-documented. The behavioral change (skip → reject for OOB bits) is the right call and is aligned with Zeam and Lantern. The Automated review by Claude (Anthropic) · sonnet · custom prompt |
Align process_attestations with leanSpec at the pinned commit (f12000b): the spec has no bitlist length check. It crashes with an IndexError when a set bit indexes past the validator list, and with an AssertionError when no bits are set; a crash aborts the state transition, invalidating the whole block. Zeam and Lantern also reject such blocks, so skipping the attestation (while accepting the block) was a chain-split vector. Replace the length-based skip with spec-faithful behavior: - a set bit at index >= validator_count rejects the block - an attestation with no participants rejects the block - oversized aggregation_bits whose set bits are all in range are processed normally (previously skipped, which also diverged)
a53108b to
f33c4ec
Compare
Problem (audit finding C1)
process_attestationsincrementedattestations_processedand inserted an empty votes entry intojustificationsbefore the oversizedaggregation_bitslength check. A skipped attestation therefore still added an all-false justification root that got serialized into the post-state: a cross-client state-divergence vector.Changes
Commit 1 moves the bounds check above the
entry()insert, so a skipped attestation leaves no trace in the post-state.Commit 2 resolves the skip-vs-reject question by checking leanSpec at the pinned commit (
f12000b,src/lean_spec/forks/lstar/spec.py::process_attestations). The spec has no bitlist length check at all. Its actual failure modes:IndexError-> STF aborts -> block rejectedError::AggregationBitsOutOfBounds)AssertionErrorinto_validator_indices-> block rejectedError::EmptyAggregationBits)Zeam and Lantern also reject such blocks (per the pre-existing in-code comment), so the previous skip-while-accepting behavior could split ethlambda off from the rest of the network on a crafted block.
The checks sit after
is_valid_vote, matching the spec's filter order: an attestation that fails the vote-validity filters is skipped without touching its bits, exactly as the speccontinues before any bits access.Testing
cargo clippy --workspace --all-targets -- -D warningsclean.missing field attestationPubkey) because the localleanSpeccheckout drifted off the pin; pre-existing and unrelated. CI regenerates fixtures from the pin.