From 4d07bfaa6deccdff94b809c18c1cd4e8dd32579b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:25:49 -0300 Subject: [PATCH 1/2] fix: check aggregation_bits length before recording the vote 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. --- crates/blockchain/state_transition/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index ff43a27f..78908c4f 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -276,13 +276,7 @@ fn process_attestations( continue; } - // Record the vote - attestations_processed += 1; - let votes = justifications - .entry(target.root) - .or_insert_with(|| std::iter::repeat_n(false, validator_count).collect()); // Reject attestations with aggregation_bits longer than the validator set. - // The spec would crash (IndexError) on OOB access; Zeam and Lantern reject. if attestation.aggregation_bits.len() > validator_count { warn!( bits_len = attestation.aggregation_bits.len(), @@ -290,6 +284,12 @@ fn process_attestations( ); continue; } + // Record the vote + attestations_processed += 1; + let votes = justifications + .entry(target.root) + .or_insert_with(|| std::iter::repeat_n(false, validator_count).collect()); + // Mark that each validator in this aggregation has voted for the target. for (validator_id, voted) in votes .iter_mut() From f33c4ec1b1556384102d3f462509825dcbfdb811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:28:06 -0300 Subject: [PATCH 2/2] fix: reject blocks with out-of-bounds or empty aggregation bits 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) --- crates/blockchain/state_transition/src/lib.rs | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/blockchain/state_transition/src/lib.rs b/crates/blockchain/state_transition/src/lib.rs index 78908c4f..ac57bc7d 100644 --- a/crates/blockchain/state_transition/src/lib.rs +++ b/crates/blockchain/state_transition/src/lib.rs @@ -37,6 +37,13 @@ pub enum Error { }, #[error("zero hash found in justifications_roots")] ZeroHashInJustificationRoots, + #[error("aggregated attestation has no participants")] + EmptyAggregationBits, + #[error("aggregation bit set at index {index} beyond validator count {validator_count}")] + AggregationBitsOutOfBounds { + index: usize, + validator_count: usize, + }, } /// Transition the given pre-state to the block's post-state. @@ -276,14 +283,26 @@ fn process_attestations( continue; } - // Reject attestations with aggregation_bits longer than the validator set. - if attestation.aggregation_bits.len() > validator_count { - warn!( - bits_len = attestation.aggregation_bits.len(), - validator_count, "Skipping attestation: aggregation_bits exceeds validator count" - ); - continue; + // The spec asserts that an aggregated attestation references at least + // one validator; the failed assert invalidates the whole block. + if attestation.aggregation_bits.count_ones() == 0 { + return Err(Error::EmptyAggregationBits); } + + // The spec indexes the per-root vote list with each participant index, + // so a set bit beyond the validator set crashes it (IndexError) and + // invalidates the whole block; Zeam and Lantern also reject such + // blocks. The spec has no bitlist length check: oversized + // aggregation_bits whose set bits are all in range process normally. + let oob_bit = (validator_count..attestation.aggregation_bits.len()) + .find(|&i| attestation.aggregation_bits.get(i) == Some(true)); + if let Some(index) = oob_bit { + return Err(Error::AggregationBitsOutOfBounds { + index, + validator_count, + }); + } + // Record the vote attestations_processed += 1; let votes = justifications