Skip to content

Commit 68740e6

Browse files
authored
fix: spec-faithful aggregation_bits bounds handling in process_attestations (#425)
## Problem (audit finding C1) `process_attestations` incremented `attestations_processed` and inserted an empty votes entry into `justifications` *before* the oversized `aggregation_bits` length 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: | Case | Spec behavior | ethlambda before | ethlambda after | |------|--------------|------------------|-----------------| | Set bit at index >= validator count | `IndexError` -> STF aborts -> **block rejected** | attestation skipped, block accepted | block rejected (`Error::AggregationBitsOutOfBounds`) | | No bits set | `AssertionError` in `to_validator_indices` -> **block rejected** | processed as no-op + spurious all-false entry in post-state | block rejected (`Error::EmptyAggregationBits`) | | Oversized bitlist, all set bits in range | processed normally | attestation skipped (post-state divergence) | processed normally | 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 spec `continue`s before any bits access. ## Testing - `cargo clippy --workspace --all-targets -- -D warnings` clean. - STF spectests currently fail to *deserialize* fixtures locally (`missing field attestationPubkey`) because the local `leanSpec` checkout drifted off the pin; pre-existing and unrelated. CI regenerates fixtures from the pin.
1 parent 47b7889 commit 68740e6

1 file changed

Lines changed: 28 additions & 9 deletions

File tree

  • crates/blockchain/state_transition/src

crates/blockchain/state_transition/src/lib.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ pub enum Error {
3737
},
3838
#[error("zero hash found in justifications_roots")]
3939
ZeroHashInJustificationRoots,
40+
#[error("aggregated attestation has no participants")]
41+
EmptyAggregationBits,
42+
#[error("aggregation bit set at index {index} beyond validator count {validator_count}")]
43+
AggregationBitsOutOfBounds {
44+
index: usize,
45+
validator_count: usize,
46+
},
4047
}
4148

4249
/// Transition the given pre-state to the block's post-state.
@@ -276,20 +283,32 @@ fn process_attestations(
276283
continue;
277284
}
278285

286+
// The spec asserts that an aggregated attestation references at least
287+
// one validator; the failed assert invalidates the whole block.
288+
if attestation.aggregation_bits.count_ones() == 0 {
289+
return Err(Error::EmptyAggregationBits);
290+
}
291+
292+
// The spec indexes the per-root vote list with each participant index,
293+
// so a set bit beyond the validator set crashes it (IndexError) and
294+
// invalidates the whole block; Zeam and Lantern also reject such
295+
// blocks. The spec has no bitlist length check: oversized
296+
// aggregation_bits whose set bits are all in range process normally.
297+
let oob_bit = (validator_count..attestation.aggregation_bits.len())
298+
.find(|&i| attestation.aggregation_bits.get(i) == Some(true));
299+
if let Some(index) = oob_bit {
300+
return Err(Error::AggregationBitsOutOfBounds {
301+
index,
302+
validator_count,
303+
});
304+
}
305+
279306
// Record the vote
280307
attestations_processed += 1;
281308
let votes = justifications
282309
.entry(target.root)
283310
.or_insert_with(|| std::iter::repeat_n(false, validator_count).collect());
284-
// Reject attestations with aggregation_bits longer than the validator set.
285-
// The spec would crash (IndexError) on OOB access; Zeam and Lantern reject.
286-
if attestation.aggregation_bits.len() > validator_count {
287-
warn!(
288-
bits_len = attestation.aggregation_bits.len(),
289-
validator_count, "Skipping attestation: aggregation_bits exceeds validator count"
290-
);
291-
continue;
292-
}
311+
293312
// Mark that each validator in this aggregation has voted for the target.
294313
for (validator_id, voted) in votes
295314
.iter_mut()

0 commit comments

Comments
 (0)