@@ -1169,21 +1169,23 @@ fn union_aggregation_bits(a: &AggregationBits, b: &AggregationBits) -> Aggregati
11691169/// - Single entry: kept as-is.
11701170/// - Multiple entries: merged into one using recursive proof aggregation
11711171/// (leanSpec PR #510).
1172+ ///
1173+ /// The input pairs each attestation with its signature proof so the two
1174+ /// Vecs can never drift out of sync (the prior signature used separate
1175+ /// `Vec<AggregatedAttestation>` and `Vec<AggregatedSignatureProof>` guarded
1176+ /// by a debug-only length assertion).
11721177fn compact_attestations (
1173- attestations : Vec < AggregatedAttestation > ,
1174- proofs : Vec < AggregatedSignatureProof > ,
1178+ entries : Vec < ( AggregatedAttestation , AggregatedSignatureProof ) > ,
11751179 head_state : & State ,
1176- ) -> Result < ( Vec < AggregatedAttestation > , Vec < AggregatedSignatureProof > ) , StoreError > {
1177- debug_assert_eq ! ( attestations. len( ) , proofs. len( ) ) ;
1178-
1179- if attestations. len ( ) <= 1 {
1180- return Ok ( ( attestations, proofs) ) ;
1180+ ) -> Result < Vec < ( AggregatedAttestation , AggregatedSignatureProof ) > , StoreError > {
1181+ if entries. len ( ) <= 1 {
1182+ return Ok ( entries) ;
11811183 }
11821184
11831185 // Group indices by AttestationData, preserving first-occurrence order
11841186 let mut order: Vec < AttestationData > = Vec :: new ( ) ;
11851187 let mut groups: HashMap < AttestationData , Vec < usize > > = HashMap :: new ( ) ;
1186- for ( i, att) in attestations . iter ( ) . enumerate ( ) {
1188+ for ( i, ( att, _ ) ) in entries . iter ( ) . enumerate ( ) {
11871189 match groups. entry ( att. data . clone ( ) ) {
11881190 std:: collections:: hash_map:: Entry :: Vacant ( e) => {
11891191 order. push ( e. key ( ) . clone ( ) ) ;
@@ -1196,23 +1198,21 @@ fn compact_attestations(
11961198 }
11971199
11981200 // Fast path: no duplicates
1199- if order. len ( ) == attestations . len ( ) {
1200- return Ok ( ( attestations , proofs ) ) ;
1201+ if order. len ( ) == entries . len ( ) {
1202+ return Ok ( entries ) ;
12011203 }
12021204
12031205 // Wrap in Option so we can .take() items by index without cloning
12041206 let mut items: Vec < Option < ( AggregatedAttestation , AggregatedSignatureProof ) > > =
1205- attestations . into_iter ( ) . zip ( proofs ) . map ( Some ) . collect ( ) ;
1207+ entries . into_iter ( ) . map ( Some ) . collect ( ) ;
12061208
1207- let mut compacted_atts = Vec :: with_capacity ( order. len ( ) ) ;
1208- let mut compacted_proofs = Vec :: with_capacity ( order. len ( ) ) ;
1209+ let mut compacted = Vec :: with_capacity ( order. len ( ) ) ;
12091210
12101211 for data in order {
12111212 let indices = & groups[ & data] ;
12121213 if indices. len ( ) == 1 {
1213- let ( att, proof) = items[ indices[ 0 ] ] . take ( ) . expect ( "index used once" ) ;
1214- compacted_atts. push ( att) ;
1215- compacted_proofs. push ( proof) ;
1214+ let item = items[ indices[ 0 ] ] . take ( ) . expect ( "index used once" ) ;
1215+ compacted. push ( item) ;
12161216 continue ;
12171217 }
12181218
@@ -1253,25 +1253,33 @@ fn compact_attestations(
12531253 . map_err ( StoreError :: SignatureAggregationFailed ) ?;
12541254
12551255 let merged_proof = AggregatedSignatureProof :: new ( merged_bits. clone ( ) , merged_proof_data) ;
1256-
1257- compacted_proofs. push ( merged_proof) ;
1258- compacted_atts. push ( AggregatedAttestation {
1256+ let merged_att = AggregatedAttestation {
12591257 aggregation_bits : merged_bits,
12601258 data,
1261- } ) ;
1259+ } ;
1260+ compacted. push ( ( merged_att, merged_proof) ) ;
12621261 }
12631262
1264- Ok ( ( compacted_atts , compacted_proofs ) )
1263+ Ok ( compacted )
12651264}
12661265
12671266/// Greedily select proofs maximizing new validator coverage.
12681267///
12691268/// For a single attestation data entry, picks proofs that cover the most
1270- /// uncovered validators. Each selected proof produces one AggregatedAttestation.
1269+ /// uncovered validators. A proof is selected as long as it adds at least
1270+ /// one previously-uncovered validator; partially-overlapping participants
1271+ /// between selected proofs are allowed. `compact_attestations` later feeds
1272+ /// these proofs as children to `aggregate_proofs`, which delegates to
1273+ /// `xmss_aggregate` — that function tracks duplicate pubkeys across
1274+ /// children via its `dup_pub_keys` machinery, so overlap is supported by
1275+ /// the underlying aggregation scheme.
1276+ ///
1277+ /// Each selected proof is appended to `selected` paired with its
1278+ /// corresponding AggregatedAttestation so the two lists can never drift
1279+ /// out of sync.
12711280fn extend_proofs_greedily (
12721281 proofs : & [ AggregatedSignatureProof ] ,
1273- selected_proofs : & mut Vec < AggregatedSignatureProof > ,
1274- attestations : & mut Vec < AggregatedAttestation > ,
1282+ selected : & mut Vec < ( AggregatedAttestation , AggregatedSignatureProof ) > ,
12751283 att_data : & AttestationData ,
12761284) {
12771285 if proofs. is_empty ( ) {
@@ -1309,16 +1317,16 @@ fn extend_proofs_greedily(
13091317 . filter ( |vid| !covered. contains ( vid) )
13101318 . collect ( ) ;
13111319
1312- attestations . push ( AggregatedAttestation {
1320+ let att = AggregatedAttestation {
13131321 aggregation_bits : proof. participants . clone ( ) ,
13141322 data : att_data. clone ( ) ,
1315- } ) ;
1316- selected_proofs. push ( proof. clone ( ) ) ;
1323+ } ;
13171324
13181325 metrics:: inc_pq_sig_aggregated_signatures ( ) ;
13191326 metrics:: inc_pq_sig_attestations_in_aggregated_signatures ( new_covered. len ( ) as u64 ) ;
13201327
13211328 covered. extend ( new_covered) ;
1329+ selected. push ( ( att, proof. clone ( ) ) ) ;
13221330 remaining_indices. remove ( & best_idx) ;
13231331 }
13241332}
@@ -1339,8 +1347,9 @@ fn build_block(
13391347 known_block_roots : & HashSet < H256 > ,
13401348 aggregated_payloads : & HashMap < H256 , ( AttestationData , Vec < AggregatedSignatureProof > ) > ,
13411349) -> Result < ( Block , Vec < AggregatedSignatureProof > , PostBlockCheckpoints ) , StoreError > {
1342- let mut aggregated_attestations: Vec < AggregatedAttestation > = Vec :: new ( ) ;
1343- let mut aggregated_signatures: Vec < AggregatedSignatureProof > = Vec :: new ( ) ;
1350+ // Paired (attestation, proof) tuples so the two can never drift out of
1351+ // sync. Unzipped once at the end for the block body and signatures list.
1352+ let mut selected: Vec < ( AggregatedAttestation , AggregatedSignatureProof ) > = Vec :: new ( ) ;
13441353
13451354 if !aggregated_payloads. is_empty ( ) {
13461355 // Genesis edge case: when building on genesis (slot 0),
@@ -1382,21 +1391,18 @@ fn build_block(
13821391 processed_data_roots. insert ( * data_root) ;
13831392 found_new = true ;
13841393
1385- extend_proofs_greedily (
1386- proofs,
1387- & mut aggregated_signatures,
1388- & mut aggregated_attestations,
1389- att_data,
1390- ) ;
1394+ extend_proofs_greedily ( proofs, & mut selected, att_data) ;
13911395 }
13921396
13931397 if !found_new {
13941398 break ;
13951399 }
13961400
13971401 // Check if justification advanced
1398- let attestations: AggregatedAttestations = aggregated_attestations
1399- . clone ( )
1402+ let attestations: AggregatedAttestations = selected
1403+ . iter ( )
1404+ . map ( |( att, _) | att. clone ( ) )
1405+ . collect :: < Vec < _ > > ( )
14001406 . try_into ( )
14011407 . expect ( "attestation count exceeds limit" ) ;
14021408 let candidate = Block {
@@ -1421,8 +1427,11 @@ fn build_block(
14211427
14221428 // Compact: merge proofs sharing the same AttestationData via recursive
14231429 // aggregation so each AttestationData appears at most once (leanSpec #510).
1424- let ( aggregated_attestations, aggregated_signatures) =
1425- compact_attestations ( aggregated_attestations, aggregated_signatures, head_state) ?;
1430+ let compacted = compact_attestations ( selected, head_state) ?;
1431+
1432+ // Unzip paired entries into the parallel lists the block expects.
1433+ let ( aggregated_attestations, aggregated_signatures) : ( Vec < _ > , Vec < _ > ) =
1434+ compacted. into_iter ( ) . unzip ( ) ;
14261435
14271436 // Build final block
14281437 let attestations: AggregatedAttestations = aggregated_attestations
@@ -1891,28 +1900,28 @@ mod tests {
18911900 let bits_a = make_bits ( & [ 0 ] ) ;
18921901 let bits_b = make_bits ( & [ 1 ] ) ;
18931902
1894- let atts = vec ! [
1895- AggregatedAttestation {
1896- aggregation_bits: bits_a. clone( ) ,
1897- data: data_a. clone( ) ,
1898- } ,
1899- AggregatedAttestation {
1900- aggregation_bits: bits_b. clone( ) ,
1901- data: data_b. clone( ) ,
1902- } ,
1903- ] ;
1904- let proofs = vec ! [
1905- AggregatedSignatureProof :: empty( bits_a) ,
1906- AggregatedSignatureProof :: empty( bits_b) ,
1903+ let entries = vec ! [
1904+ (
1905+ AggregatedAttestation {
1906+ aggregation_bits: bits_a. clone( ) ,
1907+ data: data_a. clone( ) ,
1908+ } ,
1909+ AggregatedSignatureProof :: empty( bits_a) ,
1910+ ) ,
1911+ (
1912+ AggregatedAttestation {
1913+ aggregation_bits: bits_b. clone( ) ,
1914+ data: data_b. clone( ) ,
1915+ } ,
1916+ AggregatedSignatureProof :: empty( bits_b) ,
1917+ ) ,
19071918 ] ;
19081919
19091920 let state = State :: from_genesis ( 1000 , vec ! [ ] ) ;
1910- let ( out_atts, out_proofs) =
1911- compact_attestations ( atts. clone ( ) , proofs. clone ( ) , & state) . unwrap ( ) ;
1912- assert_eq ! ( out_atts. len( ) , 2 ) ;
1913- assert_eq ! ( out_proofs. len( ) , 2 ) ;
1914- assert_eq ! ( out_atts[ 0 ] . data, data_a) ;
1915- assert_eq ! ( out_atts[ 1 ] . data, data_b) ;
1921+ let out = compact_attestations ( entries, & state) . unwrap ( ) ;
1922+ assert_eq ! ( out. len( ) , 2 ) ;
1923+ assert_eq ! ( out[ 0 ] . 0 . data, data_a) ;
1924+ assert_eq ! ( out[ 1 ] . 0 . data, data_b) ;
19161925 }
19171926
19181927 #[ test]
@@ -1925,32 +1934,36 @@ mod tests {
19251934 let bits_1 = make_bits ( & [ 1 ] ) ;
19261935 let bits_2 = make_bits ( & [ 2 ] ) ;
19271936
1928- let atts = vec ! [
1929- AggregatedAttestation {
1930- aggregation_bits: bits_0. clone( ) ,
1931- data: data_a. clone( ) ,
1932- } ,
1933- AggregatedAttestation {
1934- aggregation_bits: bits_1. clone( ) ,
1935- data: data_b. clone( ) ,
1936- } ,
1937- AggregatedAttestation {
1938- aggregation_bits: bits_2. clone( ) ,
1939- data: data_c. clone( ) ,
1940- } ,
1941- ] ;
1942- let proofs = vec ! [
1943- AggregatedSignatureProof :: empty( bits_0) ,
1944- AggregatedSignatureProof :: empty( bits_1) ,
1945- AggregatedSignatureProof :: empty( bits_2) ,
1937+ let entries = vec ! [
1938+ (
1939+ AggregatedAttestation {
1940+ aggregation_bits: bits_0. clone( ) ,
1941+ data: data_a. clone( ) ,
1942+ } ,
1943+ AggregatedSignatureProof :: empty( bits_0) ,
1944+ ) ,
1945+ (
1946+ AggregatedAttestation {
1947+ aggregation_bits: bits_1. clone( ) ,
1948+ data: data_b. clone( ) ,
1949+ } ,
1950+ AggregatedSignatureProof :: empty( bits_1) ,
1951+ ) ,
1952+ (
1953+ AggregatedAttestation {
1954+ aggregation_bits: bits_2. clone( ) ,
1955+ data: data_c. clone( ) ,
1956+ } ,
1957+ AggregatedSignatureProof :: empty( bits_2) ,
1958+ ) ,
19461959 ] ;
19471960
19481961 let state = State :: from_genesis ( 1000 , vec ! [ ] ) ;
1949- let ( out_atts , _ ) = compact_attestations ( atts , proofs , & state) . unwrap ( ) ;
1950- assert_eq ! ( out_atts . len( ) , 3 ) ;
1951- assert_eq ! ( out_atts [ 0 ] . data, data_a) ;
1952- assert_eq ! ( out_atts [ 1 ] . data, data_b) ;
1953- assert_eq ! ( out_atts [ 2 ] . data, data_c) ;
1962+ let out = compact_attestations ( entries , & state) . unwrap ( ) ;
1963+ assert_eq ! ( out . len( ) , 3 ) ;
1964+ assert_eq ! ( out [ 0 ] . 0 . data, data_a) ;
1965+ assert_eq ! ( out [ 1 ] . 0 . data, data_b) ;
1966+ assert_eq ! ( out [ 2 ] . 0 . data, data_c) ;
19541967 }
19551968
19561969 #[ test]
@@ -2036,4 +2049,65 @@ mod tests {
20362049 "Expected DuplicateAttestationData, got: {result:?}"
20372050 ) ;
20382051 }
2052+
2053+ /// A partially-overlapping proof is still selected as long as it adds at
2054+ /// least one previously-uncovered validator. The greedy prefers the
2055+ /// largest proof first, then picks additional proofs whose coverage
2056+ /// extends `covered`. The resulting overlap is handled downstream by
2057+ /// `aggregate_proofs` → `xmss_aggregate` (which tracks duplicate pubkeys
2058+ /// across children via its `dup_pub_keys` machinery).
2059+ #[ test]
2060+ fn extend_proofs_greedily_allows_overlap_when_it_adds_coverage ( ) {
2061+ let data = make_att_data ( 1 ) ;
2062+
2063+ // Distinct sizes to avoid tie-breaking ambiguity (HashSet iteration
2064+ // order differs between debug/release):
2065+ // A = {0, 1, 2, 3} (4 validators — largest, picked first)
2066+ // B = {2, 3, 4} (overlaps A on {2,3} but adds validator 4)
2067+ // C = {1, 2} (subset of A — adds nothing, must be skipped)
2068+ let proof_a = AggregatedSignatureProof :: empty ( make_bits ( & [ 0 , 1 , 2 , 3 ] ) ) ;
2069+ let proof_b = AggregatedSignatureProof :: empty ( make_bits ( & [ 2 , 3 , 4 ] ) ) ;
2070+ let proof_c = AggregatedSignatureProof :: empty ( make_bits ( & [ 1 , 2 ] ) ) ;
2071+
2072+ let mut selected = Vec :: new ( ) ;
2073+ extend_proofs_greedily ( & [ proof_a, proof_b, proof_c] , & mut selected, & data) ;
2074+
2075+ assert_eq ! (
2076+ selected. len( ) ,
2077+ 2 ,
2078+ "A and B selected (B adds validator 4); C adds nothing and is skipped"
2079+ ) ;
2080+
2081+ let covered: HashSet < u64 > = selected
2082+ . iter ( )
2083+ . flat_map ( |( _, p) | p. participant_indices ( ) )
2084+ . collect ( ) ;
2085+ assert_eq ! ( covered, HashSet :: from( [ 0 , 1 , 2 , 3 , 4 ] ) ) ;
2086+
2087+ // Attestation bits mirror the proof's participants for each entry.
2088+ for ( att, proof) in & selected {
2089+ assert_eq ! ( att. aggregation_bits, proof. participants) ;
2090+ assert_eq ! ( att. data, data) ;
2091+ }
2092+ }
2093+
2094+ /// When no proof contributes new coverage (subset of a previously selected
2095+ /// proof), greedy terminates without selecting it.
2096+ #[ test]
2097+ fn extend_proofs_greedily_stops_when_no_new_coverage ( ) {
2098+ let data = make_att_data ( 1 ) ;
2099+
2100+ // B's participants are a subset of A's. After picking A, B offers zero
2101+ // new coverage and must not be selected (its inclusion would also
2102+ // violate the disjoint invariant).
2103+ let proof_a = AggregatedSignatureProof :: empty ( make_bits ( & [ 0 , 1 , 2 , 3 ] ) ) ;
2104+ let proof_b = AggregatedSignatureProof :: empty ( make_bits ( & [ 1 , 2 ] ) ) ;
2105+
2106+ let mut selected = Vec :: new ( ) ;
2107+ extend_proofs_greedily ( & [ proof_a, proof_b] , & mut selected, & data) ;
2108+
2109+ assert_eq ! ( selected. len( ) , 1 ) ;
2110+ let covered: HashSet < u64 > = selected[ 0 ] . 1 . participant_indices ( ) . collect ( ) ;
2111+ assert_eq ! ( covered, HashSet :: from( [ 0 , 1 , 2 , 3 ] ) ) ;
2112+ }
20392113}
0 commit comments