@@ -202,11 +202,7 @@ fn select_attestations(
202202 // Running per-target-root voter set, seeded from state and updated
203203 // incrementally as entries are selected. Mirrors the role of Eth2
204204 // participation flags in Prysm/Lighthouse-style packing.
205- let mut projected = ProjectedState {
206- justified_slots : head_state. justified_slots . clone ( ) ,
207- finalized_slot : head_state. latest_finalized . slot ,
208- current_votes : build_running_votes ( head_state) ,
209- } ;
205+ let mut projected = ProjectedState :: from_head_state ( head_state) ;
210206 let mut processed_data_roots: HashSet < H256 > = HashSet :: new ( ) ;
211207
212208 // A block may carry at most `MAX_ATTESTATIONS_DATA` distinct entries
@@ -231,12 +227,6 @@ fn select_attestations(
231227 extend_proofs_greedily ( proofs, & mut selected, att_data) ;
232228
233229 let target_root = att_data. target . root ;
234- projected
235- . current_votes
236- . entry ( target_root)
237- . or_default ( )
238- . extend ( new_voters) ;
239-
240230 trace ! (
241231 tier = ?score. tier,
242232 new_voters = score. new_voters,
@@ -247,29 +237,7 @@ fn select_attestations(
247237 "selected"
248238 ) ;
249239
250- // Project justification / finalization. Finalize implies Justify
251- // (target is justified, AND source is finalized).
252- if score. tier <= Tier :: Justify {
253- justified_slots_ops:: extend_to_slot (
254- & mut projected. justified_slots ,
255- projected. finalized_slot ,
256- att_data. target . slot ,
257- ) ;
258- justified_slots_ops:: set_justified (
259- & mut projected. justified_slots ,
260- projected. finalized_slot ,
261- att_data. target . slot ,
262- ) ;
263- // Justified target's voter bucket is no longer relevant for
264- // scoring (no further entry can target it: filter rejects).
265- projected. current_votes . remove ( & target_root) ;
266- }
267- if score. tier == Tier :: Finalize {
268- let new_finalized = att_data. source . slot ;
269- let delta = new_finalized. saturating_sub ( projected. finalized_slot ) as usize ;
270- justified_slots_ops:: shift_window ( & mut projected. justified_slots , delta) ;
271- projected. finalized_slot = new_finalized;
272- }
240+ projected. advance ( score. tier , att_data, new_voters) ;
273241 }
274242
275243 selected
@@ -336,13 +304,75 @@ struct ChainContext<'a> {
336304 validator_count : usize ,
337305}
338306
339- /// Mutable projection of the post-state that `select_attestations` maintains
340- /// across rounds: which slots are justified, which slot is finalized, and the
341- /// running per-target-root voter set.
342- struct ProjectedState {
343- justified_slots : JustifiedSlots ,
344- finalized_slot : u64 ,
345- current_votes : HashMap < H256 , HashSet < u64 > > ,
307+ /// Mutable projection of the post-state that a tiered greedy selector
308+ /// maintains across rounds: which slots are justified, which slot is
309+ /// finalized, and the running per-target-root voter set.
310+ ///
311+ /// Shared by `select_attestations` (block proposal) and
312+ /// `aggregation::snapshot_aggregation_inputs` (interval-2 aggregation) so the
313+ /// two selectors project justification/finalization identically. The
314+ /// aggregator's projection is optimistic (a produced proof is not a processed
315+ /// block), but that only affects the ordering of prover work within the
316+ /// deadline, never the correctness of any produced proof.
317+ pub ( crate ) struct ProjectedState {
318+ pub ( crate ) justified_slots : JustifiedSlots ,
319+ pub ( crate ) finalized_slot : u64 ,
320+ pub ( crate ) current_votes : HashMap < H256 , HashSet < u64 > > ,
321+ }
322+
323+ impl ProjectedState {
324+ /// Seed the projection from the head state: justification/finalization as
325+ /// of the head, and the running voter set derived from the state's
326+ /// justification bitfields (see [`build_running_votes`]).
327+ pub ( crate ) fn from_head_state ( head_state : & State ) -> Self {
328+ Self {
329+ justified_slots : head_state. justified_slots . clone ( ) ,
330+ finalized_slot : head_state. latest_finalized . slot ,
331+ current_votes : build_running_votes ( head_state) ,
332+ }
333+ }
334+
335+ /// Fold a selected entry into the projection: record its voters under the
336+ /// entry's `target.root`, then advance justification/finalization per
337+ /// `tier` (Finalize implies Justify). `new_voters` is the entry's marginal
338+ /// voter set (block builder) or realized coverage (aggregator); the
339+ /// resulting per-target voter set is the same union either way.
340+ pub ( crate ) fn advance (
341+ & mut self ,
342+ tier : Tier ,
343+ att_data : & AttestationData ,
344+ new_voters : impl IntoIterator < Item = u64 > ,
345+ ) {
346+ let target_root = att_data. target . root ;
347+ self . current_votes
348+ . entry ( target_root)
349+ . or_default ( )
350+ . extend ( new_voters) ;
351+
352+ // Finalize implies Justify (target is justified, AND source is
353+ // finalized).
354+ if tier <= Tier :: Justify {
355+ justified_slots_ops:: extend_to_slot (
356+ & mut self . justified_slots ,
357+ self . finalized_slot ,
358+ att_data. target . slot ,
359+ ) ;
360+ justified_slots_ops:: set_justified (
361+ & mut self . justified_slots ,
362+ self . finalized_slot ,
363+ att_data. target . slot ,
364+ ) ;
365+ // Justified target's voter bucket is no longer relevant for
366+ // scoring (no further entry can target it: filter rejects).
367+ self . current_votes . remove ( & target_root) ;
368+ }
369+ if tier == Tier :: Finalize {
370+ let new_finalized = att_data. source . slot ;
371+ let delta = new_finalized. saturating_sub ( self . finalized_slot ) as usize ;
372+ justified_slots_ops:: shift_window ( & mut self . justified_slots , delta) ;
373+ self . finalized_slot = new_finalized;
374+ }
375+ }
346376}
347377
348378/// Validate a candidate entry against the projected chain view.
@@ -355,7 +385,7 @@ struct ProjectedState {
355385/// slot 0) is exempt from the `target.slot > source.slot` and
356386/// `target_already_justified` checks since fork-choice bootstrapping needs
357387/// it; STF will silently drop it, but it carries fork-choice signal.
358- fn entry_passes_filters (
388+ pub ( crate ) fn entry_passes_filters (
359389 att_data : & AttestationData ,
360390 known_block_roots : & HashSet < H256 > ,
361391 extended_historical_block_hashes : & [ H256 ] ,
@@ -471,7 +501,7 @@ fn score_entry(
471501/// output (`tier = Finalize` is clearer than `tier = 1`).
472502#[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord ) ]
473503#[ repr( u8 ) ]
474- enum Tier {
504+ pub ( crate ) enum Tier {
475505 /// Applying the entry crosses 2/3 on target AND finalizes the source
476506 /// (no slot strictly between source.slot and target.slot is still
477507 /// justifiable given projected finalized_slot).
@@ -499,23 +529,25 @@ enum Tier {
499529///
500530/// In both tiers `data_root` (ascending) is the final deterministic tiebreak.
501531#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
502- struct EntryScore {
503- tier : Tier ,
504- new_voters : usize ,
532+ pub ( crate ) struct EntryScore {
533+ pub ( crate ) tier : Tier ,
534+ pub ( crate ) new_voters : usize ,
535+ /// Read only inside [`EntryScore::ordering_key`]; kept private.
505536 target_slot : u64 ,
537+ /// Read only inside [`EntryScore::ordering_key`]; kept private.
506538 att_slot : u64 ,
507539}
508540
509541/// Total order over candidate entries; the smallest value is the best pick.
510542/// `tier` leads, then three tier-dependent `Reverse`-encoded priorities, then
511543/// `data_root` as the deterministic tiebreak. See [`EntryScore::ordering_key`].
512- type OrderingKey = ( Tier , Reverse < u64 > , Reverse < u64 > , Reverse < u64 > , H256 ) ;
544+ pub ( crate ) type OrderingKey = ( Tier , Reverse < u64 > , Reverse < u64 > , Reverse < u64 > , H256 ) ;
513545
514546impl EntryScore {
515547 /// Sort key where the smallest tuple is the best candidate. `tier` always
516548 /// leads; the remaining three slots carry tier-dependent priorities (see
517549 /// the type-level docs), all encoded as `Reverse` so "larger is better".
518- fn ordering_key ( & self , data_root : H256 ) -> OrderingKey {
550+ pub ( crate ) fn ordering_key ( & self , data_root : H256 ) -> OrderingKey {
519551 let more_new_voters = Reverse ( self . new_voters as u64 ) ;
520552 let newer_target = Reverse ( self . target_slot ) ;
521553 let newer_att = Reverse ( self . att_slot ) ;
0 commit comments