@@ -261,6 +261,32 @@ impl PayloadBuffer {
261261 . collect ( )
262262 }
263263
264+ /// Prune payload entries whose attestation target slot is at or below `finalized_slot`.
265+ ///
266+ /// Mirrors leanSpec's `prune_stale_attestation_data`: an entry is stale once its
267+ /// target checkpoint is finalized — it can no longer contribute to fork choice and
268+ /// keeping it around only pollutes `existing_proofs_for_data` lookups, occasionally
269+ /// forcing recursive aggregation when plain XMSS aggregation would suffice.
270+ ///
271+ /// Returns the number of data_root entries removed.
272+ fn prune ( & mut self , finalized_slot : u64 ) -> usize {
273+ let before = self . data . len ( ) ;
274+ let total_proofs = & mut self . total_proofs ;
275+ self . data . retain ( |_root, entry| {
276+ if entry. data . target . slot > finalized_slot {
277+ true
278+ } else {
279+ * total_proofs -= entry. proofs . len ( ) ;
280+ false
281+ }
282+ } ) ;
283+ let pruned = before - self . data . len ( ) ;
284+ if pruned > 0 {
285+ self . order . retain ( |r| self . data . contains_key ( r) ) ;
286+ }
287+ pruned
288+ }
289+
264290 /// Extract per-validator latest attestations from proofs' participation bits.
265291 ///
266292 /// Iterates entries in insertion order (via `self.order`) so that, when two
@@ -400,20 +426,20 @@ impl GossipSignatureBuffer {
400426 ///
401427 /// Returns the number of data_root entries pruned.
402428 fn prune ( & mut self , finalized_slot : u64 ) -> usize {
403- let mut pruned_roots : HashSet < H256 > = HashSet :: new ( ) ;
404- self . data . retain ( |root , entry| {
429+ let before = self . data . len ( ) ;
430+ self . data . retain ( |_root , entry| {
405431 if entry. data . slot > finalized_slot {
406432 true
407433 } else {
408434 self . total_signatures -= entry. signatures . len ( ) ;
409- pruned_roots. insert ( * root) ;
410435 false
411436 }
412437 } ) ;
413- if !pruned_roots. is_empty ( ) {
414- self . order . retain ( |r| !pruned_roots. contains ( r) ) ;
438+ let pruned = before - self . data . len ( ) ;
439+ if pruned > 0 {
440+ self . order . retain ( |r| self . data . contains_key ( r) ) ;
415441 }
416- pruned_roots . len ( )
442+ pruned
417443 }
418444
419445 /// Returns a snapshot of all gossip signatures grouped by attestation data.
@@ -727,11 +753,12 @@ impl Store {
727753 {
728754 let pruned_chain = self . prune_live_chain ( finalized. slot ) ;
729755 let pruned_sigs = self . prune_gossip_signatures ( finalized. slot ) ;
756+ let pruned_payloads = self . prune_stale_aggregated_payloads ( finalized. slot ) ;
730757
731- if pruned_chain > 0 || pruned_sigs > 0 {
758+ if pruned_chain > 0 || pruned_sigs > 0 || pruned_payloads > 0 {
732759 info ! (
733760 finalized_slot = finalized. slot,
734- pruned_chain, pruned_sigs, "Pruned finalized data"
761+ pruned_chain, pruned_sigs, pruned_payloads , "Pruned finalized data"
735762 ) ;
736763 }
737764 }
@@ -830,6 +857,18 @@ impl Store {
830857 gossip. prune ( finalized_slot)
831858 }
832859
860+ /// Prune aggregated payload buffers (new + known) whose target slot is at or below
861+ /// `finalized_slot`.
862+ ///
863+ /// Mirrors leanSpec's `prune_stale_attestation_data` for the two aggregated payload
864+ /// pools (gossip signatures are pruned separately by `prune_gossip_signatures`).
865+ /// Returns the total number of data_root entries removed across both buffers.
866+ pub fn prune_stale_aggregated_payloads ( & mut self , finalized_slot : u64 ) -> usize {
867+ let pruned_new = self . new_payloads . lock ( ) . unwrap ( ) . prune ( finalized_slot) ;
868+ let pruned_known = self . known_payloads . lock ( ) . unwrap ( ) . prune ( finalized_slot) ;
869+ pruned_new + pruned_known
870+ }
871+
833872 /// Prune old states beyond the retention window.
834873 ///
835874 /// Keeps the most recent `STATES_TO_KEEP` states (by slot), plus any
@@ -2075,6 +2114,95 @@ mod tests {
20752114 assert_eq ! ( buf. total_proofs, 2 ) ;
20762115 }
20772116
2117+ #[ test]
2118+ fn payload_buffer_prune_drops_entries_with_finalized_target ( ) {
2119+ let mut buf = PayloadBuffer :: new ( 10 ) ;
2120+ let target_a = H256 ( [ 0xaa ; 32 ] ) ;
2121+ let target_b = H256 ( [ 0xbb ; 32 ] ) ;
2122+ let target_c = H256 ( [ 0xcc ; 32 ] ) ;
2123+
2124+ // Three entries at different target slots: 3, 5, 7.
2125+ let data_3 = make_att_data_for_target ( 3 , target_a) ;
2126+ let data_5 = make_att_data_for_target ( 5 , target_b) ;
2127+ let data_7 = make_att_data_for_target ( 7 , target_c) ;
2128+ let root_3 = data_3. hash_tree_root ( ) ;
2129+ let root_5 = data_5. hash_tree_root ( ) ;
2130+ let root_7 = data_7. hash_tree_root ( ) ;
2131+
2132+ buf. push (
2133+ HashedAttestationData :: new ( data_3) ,
2134+ make_proof_for_validators ( & [ 0 ] ) ,
2135+ ) ;
2136+ buf. push (
2137+ HashedAttestationData :: new ( data_5) ,
2138+ make_proof_for_validators ( & [ 1 , 2 ] ) ,
2139+ ) ;
2140+ buf. push (
2141+ HashedAttestationData :: new ( data_7) ,
2142+ make_proof_for_validators ( & [ 3 ] ) ,
2143+ ) ;
2144+ assert_eq ! ( buf. total_proofs, 3 ) ;
2145+
2146+ // Finalized slot 5 prunes targets 3 and 5 (≤ 5), keeps target 7.
2147+ let pruned = buf. prune ( 5 ) ;
2148+ assert_eq ! ( pruned, 2 ) ;
2149+ assert ! ( !buf. data. contains_key( & root_3) ) ;
2150+ assert ! ( !buf. data. contains_key( & root_5) ) ;
2151+ assert ! ( buf. data. contains_key( & root_7) ) ;
2152+ assert_eq ! ( buf. total_proofs, 1 ) ;
2153+ assert_eq ! ( buf. order. len( ) , 1 ) ;
2154+ assert_eq ! ( buf. order. front( ) , Some ( & root_7) ) ;
2155+ }
2156+
2157+ #[ test]
2158+ fn payload_buffer_prune_noop_when_nothing_stale ( ) {
2159+ let mut buf = PayloadBuffer :: new ( 10 ) ;
2160+ let data = make_att_data_for_target ( 10 , H256 ( [ 0xaa ; 32 ] ) ) ;
2161+ buf. push (
2162+ HashedAttestationData :: new ( data) ,
2163+ make_proof_for_validators ( & [ 0 ] ) ,
2164+ ) ;
2165+
2166+ let pruned = buf. prune ( 5 ) ;
2167+ assert_eq ! ( pruned, 0 ) ;
2168+ assert_eq ! ( buf. total_proofs, 1 ) ;
2169+ assert_eq ! ( buf. order. len( ) , 1 ) ;
2170+ }
2171+
2172+ #[ test]
2173+ fn store_prune_stale_aggregated_payloads_clears_both_buffers ( ) {
2174+ let mut store = Store :: test_store ( ) ;
2175+
2176+ let stale = make_att_data_for_target ( 2 , H256 ( [ 0xaa ; 32 ] ) ) ;
2177+ let fresh = make_att_data_for_target ( 10 , H256 ( [ 0xbb ; 32 ] ) ) ;
2178+
2179+ store. insert_new_aggregated_payload (
2180+ HashedAttestationData :: new ( stale. clone ( ) ) ,
2181+ make_proof_for_validators ( & [ 0 ] ) ,
2182+ ) ;
2183+ store. insert_known_aggregated_payload (
2184+ HashedAttestationData :: new ( stale) ,
2185+ make_proof_for_validators ( & [ 1 ] ) ,
2186+ ) ;
2187+ store. insert_new_aggregated_payload (
2188+ HashedAttestationData :: new ( fresh. clone ( ) ) ,
2189+ make_proof_for_validators ( & [ 2 ] ) ,
2190+ ) ;
2191+ store. insert_known_aggregated_payload (
2192+ HashedAttestationData :: new ( fresh) ,
2193+ make_proof_for_validators ( & [ 3 ] ) ,
2194+ ) ;
2195+
2196+ assert_eq ! ( store. new_aggregated_payloads_count( ) , 2 ) ;
2197+ assert_eq ! ( store. known_aggregated_payloads_count( ) , 2 ) ;
2198+
2199+ // Finalized slot 5: stale (target.slot == 2) is dropped from both buffers.
2200+ let pruned = store. prune_stale_aggregated_payloads ( 5 ) ;
2201+ assert_eq ! ( pruned, 2 ) ;
2202+ assert_eq ! ( store. new_aggregated_payloads_count( ) , 1 ) ;
2203+ assert_eq ! ( store. known_aggregated_payloads_count( ) , 1 ) ;
2204+ }
2205+
20782206 /// Build an attestation message at `slot` whose target points at `target_root`,
20792207 /// distinct from the default zero target so two such datas have different roots.
20802208 fn make_att_data_for_target ( slot : u64 , target_root : H256 ) -> AttestationData {
0 commit comments