@@ -613,6 +613,73 @@ impl<F: Family, D: Digest> Proof<F, D> {
613613 )
614614 . ok_or ( ReconstructionError :: InvalidProof )
615615 }
616+
617+ /// Authenticate the proven range without reconstructing the full generic Merkle root.
618+ ///
619+ /// This consumes only the sibling digests needed to rebuild the proven range peaks, then returns
620+ /// those peak digests in `collected`. It deliberately does not consume peak-bagging witnesses
621+ /// such as prefix peaks, after peaks, or backward-fold suffix accumulators.
622+ ///
623+ /// Current QMDB grafted proofs use this path because their final root is rebuilt by the wrapper
624+ /// from the collected range peaks plus grafted prefix/suffix witnesses. Including generic
625+ /// peak-bagging witnesses here would create proof bytes that the wrapper root ignores, making
626+ /// those bytes malleable.
627+ pub ( crate ) fn reconstruct_range_collecting < H , E > (
628+ & self ,
629+ hasher : & H ,
630+ elements : & [ E ] ,
631+ start_loc : Location < F > ,
632+ collected : & mut Vec < ( Position < F > , D ) > ,
633+ ) -> Result < ( ) , ReconstructionError >
634+ where
635+ H : Hasher < F , Digest = D > ,
636+ E : AsRef < [ u8 ] > ,
637+ {
638+ if elements. is_empty ( ) {
639+ return Err ( ReconstructionError :: MissingElements ) ;
640+ }
641+ if !start_loc. is_valid_index ( ) {
642+ return Err ( ReconstructionError :: InvalidStartLoc ) ;
643+ }
644+ let end_loc = start_loc
645+ . checked_add ( elements. len ( ) as u64 )
646+ . ok_or ( ReconstructionError :: InvalidEndLoc ) ?;
647+ if end_loc > self . leaves {
648+ return Err ( ReconstructionError :: InvalidEndLoc ) ;
649+ }
650+
651+ let range = start_loc..end_loc;
652+ // Bagging only governs the prefix/suffix accumulator layout, which this method
653+ // deliberately ignores; `range_peaks` and `fetch_nodes` are bagging-independent. Pass
654+ // ForwardFold as a placeholder.
655+ let bp = Blueprint :: new (
656+ self . leaves ,
657+ self . inactive_peaks ,
658+ Bagging :: ForwardFold ,
659+ range,
660+ )
661+ . map_err ( |_| ReconstructionError :: InvalidSize ) ?;
662+
663+ let mut sibling_cursor = 0usize ;
664+ let mut elements_iter = elements. iter ( ) ;
665+ for peak in & bp. range_peaks {
666+ let peak_digest = peak. reconstruct_digest (
667+ hasher,
668+ & bp. range ,
669+ & mut elements_iter,
670+ & self . digests ,
671+ & mut sibling_cursor,
672+ None ,
673+ ) ?;
674+ collected. push ( ( peak. pos , peak_digest) ) ;
675+ }
676+
677+ if elements_iter. next ( ) . is_some ( ) || sibling_cursor != self . digests . len ( ) {
678+ return Err ( ReconstructionError :: ExtraDigests ) ;
679+ }
680+
681+ Ok ( ( ) )
682+ }
616683}
617684
618685/// A perfect binary subtree within a peak, identified by its root position, height,
@@ -1087,6 +1154,56 @@ where
10871154 )
10881155}
10891156
1157+ /// Return the node positions needed by [`build_range_collection_proof`].
1158+ ///
1159+ /// Bagging only governs prefix/suffix accumulator layout, which this helper does not consult; the
1160+ /// `range_peaks` siblings it collects are bagging-independent. ForwardFold is passed as a
1161+ /// placeholder.
1162+ #[ cfg( feature = "std" ) ]
1163+ pub ( crate ) fn range_collection_nodes < F : Family > (
1164+ leaves : Location < F > ,
1165+ inactive_peaks : usize ,
1166+ range : Range < Location < F > > ,
1167+ ) -> Result < Vec < Position < F > > , super :: Error < F > > {
1168+ let bp = Blueprint :: new ( leaves, inactive_peaks, Bagging :: ForwardFold , range) ?;
1169+ let mut fetch_nodes = Vec :: new ( ) ;
1170+ for peak in & bp. range_peaks {
1171+ peak. collect_siblings ( & bp. range , & mut fetch_nodes) ;
1172+ }
1173+ Ok ( fetch_nodes)
1174+ }
1175+
1176+ /// Build a proof containing only the sibling digests needed to authenticate the requested range.
1177+ ///
1178+ /// The resulting proof cannot reconstruct a complete generic Merkle root on its own. It is intended
1179+ /// for wrappers that rebuild a custom root from separately supplied peak witnesses, such as current
1180+ /// QMDB grafted proofs.
1181+ #[ cfg( feature = "std" ) ]
1182+ pub ( crate ) fn build_range_collection_proof < F , D , E > (
1183+ leaves : Location < F > ,
1184+ inactive_peaks : usize ,
1185+ range : Range < Location < F > > ,
1186+ get_node : impl Fn ( Position < F > ) -> Option < D > ,
1187+ element_pruned : impl Fn ( Position < F > ) -> E ,
1188+ ) -> Result < Proof < F , D > , E >
1189+ where
1190+ F : Family ,
1191+ D : Digest ,
1192+ E : From < super :: Error < F > > ,
1193+ {
1194+ let fetch_nodes = range_collection_nodes ( leaves, inactive_peaks, range) ?;
1195+ let mut digests = Vec :: with_capacity ( fetch_nodes. len ( ) ) ;
1196+ for pos in fetch_nodes {
1197+ digests. push ( get_node ( pos) . ok_or_else ( || element_pruned ( pos) ) ?) ;
1198+ }
1199+
1200+ Ok ( Proof {
1201+ leaves,
1202+ inactive_peaks,
1203+ digests,
1204+ } )
1205+ }
1206+
10901207/// Returns the positions of the minimal set of nodes whose digests are required to prove the
10911208/// inclusion of the elements at the specified `locations`, using the provided root bagging.
10921209#[ cfg( any( feature = "std" , test) ) ]
0 commit comments