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