Skip to content

Commit 2bb7ac7

Browse files
committed
refactor(blockchain): extract shared ProjectedState from block builder
Pull the round-by-round justification/finalization projection out of select_attestations into a reusable ProjectedState with from_head_state() and advance(), and widen entry_passes_filters, Tier, EntryScore, OrderingKey, and EntryScore::ordering_key to pub(crate). Block-building behavior is unchanged (the advance() body is the former inline projection, and the current_votes update simply moves after the trace it never fed). This exposes the projection and scoring primitives so interval-2 aggregation can reuse them instead of duplicating the justify/finalize logic.
1 parent c61165a commit 2bb7ac7

1 file changed

Lines changed: 80 additions & 48 deletions

File tree

crates/blockchain/src/block_builder.rs

Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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

514546
impl 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

Comments
 (0)