From 945fb88dab6c35ad7a102e5c70b6d6a3cc02f986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:16:53 -0300 Subject: [PATCH] feat(blockchain): aggregate only current-slot raw signatures at interval 2 The interval-2 aggregation worker processed jobs in HashMap-iteration order (gossip groups then payload-only merges) with no slot ordering, so the 750ms deadline could drop current-slot work in favor of stale-slot or recursive-merge jobs. Since only the current slot's votes get promoted and packed into the next block, that budget was misspent. Add snapshot_current_slot_aggregation_inputs: it builds pure raw-signature jobs for gossip groups whose data.slot matches the current slot only, with no existing-proof reuse and no payload-only pass. Wire it into start_aggregation_session. snapshot_aggregation_inputs is left intact. --- crates/blockchain/src/aggregation.rs | 94 ++++++++++++++++++++++++++++ crates/blockchain/src/lib.rs | 6 +- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/crates/blockchain/src/aggregation.rs b/crates/blockchain/src/aggregation.rs index 2442aea6..a6d050a4 100644 --- a/crates/blockchain/src/aggregation.rs +++ b/crates/blockchain/src/aggregation.rs @@ -172,6 +172,51 @@ pub fn snapshot_aggregation_inputs(store: &Store) -> Option }) } +/// Build a snapshot that aggregates *only* the current slot's raw gossip +/// signatures. +/// +/// Unlike [`snapshot_aggregation_inputs`], this deliberately ignores existing +/// proofs and payload-only groups: every job is a pure raw-signature +/// aggregation over the gossip signatures whose `data.slot` matches +/// `current_slot`. Stale-slot gossip groups are left untouched so the +/// interval-2 deadline is never spent re-aggregating past slots. +/// +/// Returns `None` when there is nothing to aggregate for `current_slot` so +/// callers can avoid spawning an empty worker. +pub fn snapshot_current_slot_aggregation_inputs( + store: &Store, + current_slot: u64, +) -> Option { + let gossip_groups = store.iter_gossip_signatures(); + if gossip_groups.is_empty() { + return None; + } + + let head_state = store.head_state(); + let validators = &head_state.validators; + + let mut jobs = Vec::new(); + let mut groups_considered = 0usize; + for (hashed, validator_sigs) in &gossip_groups { + if hashed.data().slot != current_slot { + continue; + } + groups_considered += 1; + if let Some(job) = build_raw_signature_job(validators, hashed.clone(), validator_sigs) { + jobs.push(job); + } + } + + if jobs.is_empty() { + return None; + } + + Some(AggregationSnapshot { + jobs, + groups_considered, + }) +} + /// Build one `AggregationJob` for a given attestation data. Returns `None` when /// there is not enough material for a viable aggregation (no raw sigs and fewer /// than two children). `validator_sigs` is `None` for Pass 2 (payload-only). @@ -228,6 +273,55 @@ fn build_job( }) } +/// Build a raw-signature-only `AggregationJob`: no existing-proof reuse and no +/// children. Every resolvable gossip signature in the group becomes a raw +/// participant. Returns `None` when no signature resolves to a pubkey. +fn build_raw_signature_job( + validators: &[Validator], + hashed: HashedAttestationData, + validator_sigs: &[(u64, ValidatorSignature)], +) -> Option { + let data_root = hashed.root(); + + let mut raw_sigs = Vec::with_capacity(validator_sigs.len()); + let mut raw_pubkeys = Vec::with_capacity(validator_sigs.len()); + let mut raw_ids = Vec::with_capacity(validator_sigs.len()); + for (vid, sig) in validator_sigs { + let Some(validator) = validators.get(*vid as usize) else { + continue; + }; + let Ok(pubkey) = validator.get_attestation_pubkey() else { + continue; + }; + raw_sigs.push(sig.clone()); + raw_pubkeys.push(pubkey); + raw_ids.push(*vid); + } + + if raw_ids.is_empty() { + return None; + } + + // Consume the whole group's gossip signatures on successful aggregation, + // mirroring `build_job`. + let keys_to_delete: Vec<(u64, H256)> = validator_sigs + .iter() + .map(|(vid, _)| (*vid, data_root)) + .collect(); + + let slot = hashed.data().slot; + Some(AggregationJob { + hashed, + slot, + children: Vec::new(), + accepted_child_ids: Vec::new(), + raw_pubkeys, + raw_sigs, + raw_ids, + keys_to_delete, + }) +} + /// Resolve each child's participant pubkeys. Drops any child whose pubkeys /// can't be fully resolved (passing fewer pubkeys than the proof expects would /// produce an invalid aggregate). diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 89a1a1d5..fdbb815b 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -388,8 +388,10 @@ impl BlockChainServer { } } - let Some(snapshot) = aggregation::snapshot_aggregation_inputs(&self.store) else { - // No gossip sigs and no pending payloads — nothing to aggregate this slot. + let Some(snapshot) = + aggregation::snapshot_current_slot_aggregation_inputs(&self.store, slot) + else { + // No current-slot gossip sigs — nothing to aggregate this slot. return; };