@@ -592,6 +592,74 @@ impl<F: Family, D: Digest> Proof<F, D> {
592592 )
593593 . ok_or ( ReconstructionError :: InvalidProof )
594594 }
595+
596+ /// Authenticate the proven range without reconstructing the full generic Merkle root.
597+ ///
598+ /// This consumes only the sibling digests needed to rebuild the proven range peaks, then returns
599+ /// those peak digests in `collected`. It deliberately does not consume peak-bagging witnesses
600+ /// such as prefix peaks, after peaks, or backward-fold suffix accumulators.
601+ ///
602+ /// Current QMDB grafted proofs use this path because their final root is rebuilt by the wrapper
603+ /// from the collected range peaks plus grafted prefix/suffix witnesses. Including generic
604+ /// peak-bagging witnesses here would create proof bytes that the wrapper root ignores, making
605+ /// those bytes malleable.
606+ #[ cfg( feature = "std" ) ]
607+ pub ( crate ) fn reconstruct_range_collecting < H , E > (
608+ & self ,
609+ hasher : & H ,
610+ elements : & [ E ] ,
611+ start_loc : Location < F > ,
612+ collected : & mut Vec < ( Position < F > , D ) > ,
613+ ) -> Result < ( ) , ReconstructionError >
614+ where
615+ H : Hasher < F , Digest = D > ,
616+ E : AsRef < [ u8 ] > ,
617+ {
618+ if elements. is_empty ( ) {
619+ return Err ( ReconstructionError :: MissingElements ) ;
620+ }
621+ if !start_loc. is_valid_index ( ) {
622+ return Err ( ReconstructionError :: InvalidStartLoc ) ;
623+ }
624+ let end_loc = start_loc
625+ . checked_add ( elements. len ( ) as u64 )
626+ . ok_or ( ReconstructionError :: InvalidEndLoc ) ?;
627+ if end_loc > self . leaves {
628+ return Err ( ReconstructionError :: InvalidEndLoc ) ;
629+ }
630+
631+ let range = start_loc..end_loc;
632+ // Bagging only governs the prefix/suffix accumulator layout, which this method
633+ // deliberately ignores; `range_peaks` and `fetch_nodes` are bagging-independent. Pass
634+ // ForwardFold as a placeholder.
635+ let bp = Blueprint :: new (
636+ self . leaves ,
637+ self . inactive_peaks ,
638+ Bagging :: ForwardFold ,
639+ range,
640+ )
641+ . map_err ( |_| ReconstructionError :: InvalidSize ) ?;
642+
643+ let mut sibling_cursor = 0usize ;
644+ let mut elements_iter = elements. iter ( ) ;
645+ for peak in & bp. range_peaks {
646+ let peak_digest = peak. reconstruct_digest (
647+ hasher,
648+ & bp. range ,
649+ & mut elements_iter,
650+ & self . digests ,
651+ & mut sibling_cursor,
652+ None ,
653+ ) ?;
654+ collected. push ( ( peak. pos , peak_digest) ) ;
655+ }
656+
657+ if elements_iter. next ( ) . is_some ( ) || sibling_cursor != self . digests . len ( ) {
658+ return Err ( ReconstructionError :: ExtraDigests ) ;
659+ }
660+
661+ Ok ( ( ) )
662+ }
595663}
596664
597665/// A perfect binary subtree within a peak, identified by its root position, height,
@@ -1066,6 +1134,56 @@ where
10661134 )
10671135}
10681136
1137+ /// Return the node positions needed by [`build_range_collection_proof`].
1138+ ///
1139+ /// Bagging only governs prefix/suffix accumulator layout, which this helper does not consult; the
1140+ /// `range_peaks` siblings it collects are bagging-independent. ForwardFold is passed as a
1141+ /// placeholder.
1142+ #[ cfg( feature = "std" ) ]
1143+ pub ( crate ) fn range_collection_nodes < F : Family > (
1144+ leaves : Location < F > ,
1145+ inactive_peaks : usize ,
1146+ range : Range < Location < F > > ,
1147+ ) -> Result < Vec < Position < F > > , super :: Error < F > > {
1148+ let bp = Blueprint :: new ( leaves, inactive_peaks, Bagging :: ForwardFold , range) ?;
1149+ let mut fetch_nodes = Vec :: new ( ) ;
1150+ for peak in & bp. range_peaks {
1151+ peak. collect_siblings ( & bp. range , & mut fetch_nodes) ;
1152+ }
1153+ Ok ( fetch_nodes)
1154+ }
1155+
1156+ /// Build a proof containing only the sibling digests needed to authenticate the requested range.
1157+ ///
1158+ /// The resulting proof cannot reconstruct a complete generic Merkle root on its own. It is intended
1159+ /// for wrappers that rebuild a custom root from separately supplied peak witnesses, such as current
1160+ /// QMDB grafted proofs.
1161+ #[ cfg( feature = "std" ) ]
1162+ pub ( crate ) fn build_range_collection_proof < F , D , E > (
1163+ leaves : Location < F > ,
1164+ inactive_peaks : usize ,
1165+ range : Range < Location < F > > ,
1166+ get_node : impl Fn ( Position < F > ) -> Option < D > ,
1167+ element_pruned : impl Fn ( Position < F > ) -> E ,
1168+ ) -> Result < Proof < F , D > , E >
1169+ where
1170+ F : Family ,
1171+ D : Digest ,
1172+ E : From < super :: Error < F > > ,
1173+ {
1174+ let fetch_nodes = range_collection_nodes ( leaves, inactive_peaks, range) ?;
1175+ let mut digests = Vec :: with_capacity ( fetch_nodes. len ( ) ) ;
1176+ for pos in fetch_nodes {
1177+ digests. push ( get_node ( pos) . ok_or_else ( || element_pruned ( pos) ) ?) ;
1178+ }
1179+
1180+ Ok ( Proof {
1181+ leaves,
1182+ inactive_peaks,
1183+ digests,
1184+ } )
1185+ }
1186+
10691187/// Returns the positions of the minimal set of nodes whose digests are required to prove the
10701188/// inclusion of the elements at the specified `locations`, using the provided root bagging.
10711189#[ cfg( any( feature = "std" , test) ) ]
0 commit comments