Skip to content

Commit 29cc787

Browse files
committed
fix(state-transition): bound distinct attestation data in the transition
The per-block cap on distinct AttestationData is a transition rule in leanSpec (`process_attestations`), and `fork_choice.on_block` says so explicitly: "The transition itself bounds the distinct-data count. Only the wire-level duplicate prohibition lives here." We enforced it only at the import boundary in `on_block`, so `state_transition()` accepted an over-cap block and then failed on the state root instead. Both block production (`build_block` -> `process_block`) and spec-fixture replay call the transition without going through `on_block`, so neither was bounded. Check it at the top of `process_attestations`, ahead of the justification-bookkeeping guards as the spec does. The `on_block` check stays for now: it runs before signature verification, so an over-cap block is still rejected without paying for proof verification. (leanSpec #536)
1 parent fc1f83a commit 29cc787

1 file changed

Lines changed: 22 additions & 2 deletions

File tree

  • crates/blockchain/state_transition/src

crates/blockchain/state_transition/src/lib.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use std::collections::HashMap;
1+
use std::collections::{HashMap, HashSet};
22

33
use ethlambda_types::{
44
ShortRoot,
55
attestation::AttestationData,
6-
block::{AggregatedAttestations, Block, BlockHeader},
6+
block::{AggregatedAttestations, Block, BlockHeader, MAX_ATTESTATIONS_DATA},
77
checkpoint::Checkpoint,
88
primitives::{H256, HashTreeRoot as _},
99
state::{HISTORICAL_ROOTS_LIMIT, JustificationValidators, State},
@@ -48,6 +48,8 @@ pub enum Error {
4848
index: usize,
4949
validator_count: usize,
5050
},
51+
#[error("block carries {count} distinct AttestationData entries; maximum is {max}")]
52+
TooManyAttestationData { count: usize, max: usize },
5153
}
5254

5355
/// Transition the given pre-state to the block's post-state.
@@ -243,6 +245,24 @@ fn process_attestations(
243245
) -> Result<(), Error> {
244246
let _timing = metrics::time_attestations_processing();
245247

248+
// Cap the distinct attestation data a block may carry (leanSpec #536).
249+
//
250+
// Each distinct data allocates a tally sized to the validator set below, so the
251+
// distinct count, not the attestation count, is what drives the work here. Split
252+
// aggregates over one data share their tally and count once.
253+
//
254+
// `on_block` re-checks this before signature verification so a crafted block is
255+
// rejected cheaply, but the bound belongs to the transition: this is the only
256+
// entry point block production and fixture replay share with block import.
257+
let distinct_attestation_data: HashSet<&AttestationData> =
258+
attestations.iter().map(|att| &att.data).collect();
259+
if distinct_attestation_data.len() > MAX_ATTESTATIONS_DATA {
260+
return Err(Error::TooManyAttestationData {
261+
count: distinct_attestation_data.len(),
262+
max: MAX_ATTESTATIONS_DATA,
263+
});
264+
}
265+
246266
// Validate the justification bookkeeping before unpacking the flat vote list
247267
// (leanSpec #1178). A `State` decoded from untrusted bytes (e.g. checkpoint
248268
// sync) can satisfy SSZ yet still violate these cross-field invariants; without

0 commit comments

Comments
 (0)