@@ -236,9 +236,11 @@ impl<D: Digest> Proof<D> {
236236 if bp. fold_prefix . is_empty ( ) { 0 } else { 1 } + bp. fetch_nodes . len ( ) ,
237237 ) ;
238238 if !bp. fold_prefix . is_empty ( ) {
239- // Fold prefix peaks into accumulator
240- let mut acc = hasher. digest ( & self . leaves . to_be_bytes ( ) ) ;
241- for & pos in & bp. fold_prefix {
239+ // Fold prefix peaks into accumulator (without the leaf count).
240+ let mut acc = * node_digests
241+ . get ( & bp. fold_prefix [ 0 ] )
242+ . expect ( "must exist by construction" ) ;
243+ for & pos in & bp. fold_prefix [ 1 ..] {
242244 let d = node_digests. get ( & pos) . expect ( "must exist by construction" ) ;
243245 acc = hasher. fold ( & acc, d) ;
244246 }
@@ -350,13 +352,17 @@ impl<D: Digest> Proof<D> {
350352 . zip ( pinned_nodes. iter ( ) . copied ( ) )
351353 . collect ( ) ;
352354
353- // Verify fold-prefix pinned nodes by recomputing the accumulator.
355+ // Verify fold-prefix pinned nodes by recomputing the accumulator (without the leaf
356+ // count, which is hashed into the final root independently).
354357 if !bp. fold_prefix . is_empty ( ) {
355358 if self . digests . is_empty ( ) {
356359 return false ;
357360 }
358- let mut acc = hasher. digest ( & self . leaves . to_be_bytes ( ) ) ;
359- for pos in & bp. fold_prefix {
361+ let Some ( first) = pinned_map. remove ( & bp. fold_prefix [ 0 ] ) else {
362+ return false ;
363+ } ;
364+ let mut acc = first;
365+ for pos in & bp. fold_prefix [ 1 ..] {
360366 let Some ( digest) = pinned_map. remove ( pos) else {
361367 return false ;
362368 } ;
@@ -462,12 +468,12 @@ impl<D: Digest> Proof<D> {
462468 let after_end = after_start + after_peaks. len ( ) ;
463469 let siblings = & self . digests [ after_end..] ;
464470
465- // Start fold accumulator.
466- let mut acc = if has_prefix {
467- // The first digest is the pre-folded prefix accumulator.
468- self . digests [ 0 ]
471+ // Fold all peaks into an accumulator (without the leaf count, which is hashed in at the
472+ // end to prevent malleability via the `leaves` field).
473+ let mut acc : Option < D > = if has_prefix {
474+ Some ( self . digests [ 0 ] )
469475 } else {
470- hasher . digest ( & self . leaves . to_be_bytes ( ) )
476+ None
471477 } ;
472478
473479 // Reconstruct each range peak and fold into acc.
@@ -490,7 +496,7 @@ impl<D: Digest> Proof<D> {
490496 if let Some ( ref mut cd) = collected_digests {
491497 cd. push ( ( peak_pos, peak_digest) ) ;
492498 }
493- acc = hasher. fold ( & acc , & peak_digest) ;
499+ acc = Some ( acc . map_or ( peak_digest , |a| hasher. fold ( & a , & peak_digest) ) ) ;
494500 }
495501
496502 // Fold after-peak digests.
@@ -499,7 +505,7 @@ impl<D: Digest> Proof<D> {
499505 if let Some ( ref mut cd) = collected_digests {
500506 cd. push ( ( after_peak_pos, digest) ) ;
501507 }
502- acc = hasher. fold ( & acc , & digest) ;
508+ acc = Some ( acc . map_or ( digest , |a| hasher. fold ( & a , & digest) ) ) ;
503509 }
504510
505511 // Verify all elements were consumed.
@@ -512,7 +518,12 @@ impl<D: Digest> Proof<D> {
512518 return Err ( ReconstructionError :: ExtraDigests ) ;
513519 }
514520
515- Ok ( acc)
521+ // Hash the leaf count into the final result.
522+ Ok ( if let Some ( peaks_acc) = acc {
523+ hasher. hash ( [ self . leaves . to_be_bytes ( ) . as_slice ( ) , peaks_acc. as_ref ( ) ] )
524+ } else {
525+ hasher. digest ( & self . leaves . to_be_bytes ( ) )
526+ } )
516527 }
517528}
518529
@@ -640,10 +651,11 @@ where
640651 let mut digests =
641652 Vec :: with_capacity ( if bp. fold_prefix . is_empty ( ) { 0 } else { 1 } + bp. fetch_nodes . len ( ) ) ;
642653
643- // Fold prefix peaks into a single accumulator.
654+ // Fold prefix peaks into a single accumulator (without the leaf count, which is always
655+ // hashed into the final root independently).
644656 if !bp. fold_prefix . is_empty ( ) {
645- let mut acc = hasher . digest ( & leaves . to_be_bytes ( ) ) ;
646- for & pos in & bp. fold_prefix {
657+ let mut acc = get_node ( bp . fold_prefix [ 0 ] ) . ok_or ( Error :: ElementPruned ( bp . fold_prefix [ 0 ] ) ) ? ;
658+ for & pos in & bp. fold_prefix [ 1 .. ] {
647659 let d = get_node ( pos) . ok_or ( Error :: ElementPruned ( pos) ) ?;
648660 acc = hasher. fold ( & acc, & d) ;
649661 }
@@ -1822,6 +1834,43 @@ mod tests {
18221834 ) ;
18231835 }
18241836
1837+ /// Regression test: mutating only the `leaves` field in a proof must invalidate it.
1838+ /// Before the fix, when a fold prefix existed, the leaf count was baked into the
1839+ /// pre-folded accumulator but not independently checked during verification, so a
1840+ /// different `leaves` value with a compatible peak structure would still verify.
1841+ #[ test]
1842+ fn test_proof_leaves_malleability ( ) {
1843+ let mut hasher: Standard < Sha256 > = Standard :: new ( ) ;
1844+ let mut mmr = Mmr :: new ( & mut hasher) ;
1845+
1846+ // 252 leaves. Leaf 240 sits in a peak preceded by 4 prefix peaks.
1847+ let elements: Vec < Digest > = ( 0 ..252u16 )
1848+ . map ( |i| Sha256 :: hash ( & i. to_be_bytes ( ) ) )
1849+ . collect ( ) ;
1850+ let changeset = {
1851+ let mut batch = mmr. new_batch ( ) ;
1852+ for e in & elements {
1853+ batch. add ( & mut hasher, e) ;
1854+ }
1855+ batch. merkleize ( & mut hasher) . finalize ( )
1856+ } ;
1857+ mmr. apply ( changeset) . unwrap ( ) ;
1858+ let root = mmr. root ( ) ;
1859+
1860+ let loc = Location :: new ( 240 ) ;
1861+ let proof = mmr. proof ( & mut hasher, loc) . unwrap ( ) ;
1862+ assert ! ( proof. verify_element_inclusion( & mut hasher, & elements[ 240 ] , loc, root) ) ;
1863+
1864+ // Tamper with the leaves field (249 has the same peak layout for leaf 240).
1865+ let mut tampered = proof. clone ( ) ;
1866+ tampered. leaves = Location :: new ( 249 ) ;
1867+ assert_ne ! ( tampered, proof) ;
1868+ assert ! (
1869+ !tampered. verify_element_inclusion( & mut hasher, & elements[ 240 ] , loc, root) ,
1870+ "proof with tampered leaves field must not verify"
1871+ ) ;
1872+ }
1873+
18251874 #[ cfg( feature = "arbitrary" ) ]
18261875 mod conformance {
18271876 use super :: * ;
0 commit comments