Skip to content

Commit 571016e

Browse files
support split bagging in qmdb-current
1 parent df9c036 commit 571016e

17 files changed

Lines changed: 1104 additions & 370 deletions

File tree

storage/fuzz/fuzz_targets/current_ordered_operations.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ enum CurrentOperation {
5353
bad_digests: Vec<[u8; 32]>,
5454
max_ops: NonZeroU64,
5555
bad_chunks: Vec<[u8; 32]>,
56+
bad_prefix_peaks: Vec<[u8; 32]>,
57+
bad_suffix_peaks: Vec<[u8; 32]>,
5658
},
5759
GetSpan {
5860
key: RawKey,
@@ -269,7 +271,14 @@ fn fuzz_family<F: Graftable + RootSpec>(data: &FuzzInput, suffix: &str) {
269271
}
270272
}
271273

272-
CurrentOperation::ArbitraryProof {start_loc, bad_digests, max_ops, bad_chunks} => {
274+
CurrentOperation::ArbitraryProof {
275+
start_loc,
276+
bad_digests,
277+
max_ops,
278+
bad_chunks,
279+
bad_prefix_peaks,
280+
bad_suffix_peaks,
281+
} => {
273282
let current_op_count = db.bounds().await.end;
274283
if current_op_count == 0 {
275284
continue;
@@ -313,6 +322,36 @@ fn fuzz_family<F: Graftable + RootSpec>(data: &FuzzInput, suffix: &str) {
313322
&root
314323
), "proof with bad chunks should not verify");
315324
}
325+
326+
let bad_prefix_peaks =
327+
bad_prefix_peaks.iter().map(|d| Digest::from(*d)).collect();
328+
if range_proof.unfolded_prefix_peaks != bad_prefix_peaks {
329+
let mut bad_proof = range_proof.clone();
330+
bad_proof.unfolded_prefix_peaks = bad_prefix_peaks;
331+
assert!(!Db::<F>::verify_range_proof(
332+
&mut hasher,
333+
&bad_proof,
334+
start_loc,
335+
&ops,
336+
&chunks,
337+
&root
338+
), "proof with bad prefix peaks should not verify");
339+
}
340+
341+
let bad_suffix_peaks =
342+
bad_suffix_peaks.iter().map(|d| Digest::from(*d)).collect();
343+
if range_proof.unfolded_suffix_peaks != bad_suffix_peaks {
344+
let mut bad_proof = range_proof.clone();
345+
bad_proof.unfolded_suffix_peaks = bad_suffix_peaks;
346+
assert!(!Db::<F>::verify_range_proof(
347+
&mut hasher,
348+
&bad_proof,
349+
start_loc,
350+
&ops,
351+
&chunks,
352+
&root
353+
), "proof with bad suffix peaks should not verify");
354+
}
316355
}
317356
}
318357

storage/fuzz/fuzz_targets/current_unordered_operations.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ enum CurrentOperation {
5353
bad_digests: Vec<[u8; 32]>,
5454
max_ops: NonZeroU64,
5555
bad_chunks: Vec<[u8; 32]>,
56+
bad_prefix_peaks: Vec<[u8; 32]>,
57+
bad_suffix_peaks: Vec<[u8; 32]>,
5658
},
5759
}
5860

@@ -244,7 +246,14 @@ fn fuzz_family<F: Graftable + RootSpec>(data: &FuzzInput, suffix: &str) {
244246
}
245247
}
246248

247-
CurrentOperation::ArbitraryProof {start_loc, bad_digests, max_ops, bad_chunks} => {
249+
CurrentOperation::ArbitraryProof {
250+
start_loc,
251+
bad_digests,
252+
max_ops,
253+
bad_chunks,
254+
bad_prefix_peaks,
255+
bad_suffix_peaks,
256+
} => {
248257
let current_op_count = db.bounds().await.end;
249258
if current_op_count == 0 {
250259
continue;
@@ -285,6 +294,36 @@ fn fuzz_family<F: Graftable + RootSpec>(data: &FuzzInput, suffix: &str) {
285294
&root
286295
), "proof with bad chunks should not verify");
287296
}
297+
298+
let bad_prefix_peaks =
299+
bad_prefix_peaks.iter().map(|d| Digest::from(*d)).collect();
300+
if range_proof.unfolded_prefix_peaks != bad_prefix_peaks {
301+
let mut bad_proof = range_proof.clone();
302+
bad_proof.unfolded_prefix_peaks = bad_prefix_peaks;
303+
assert!(!Db::<F>::verify_range_proof(
304+
&mut hasher,
305+
&bad_proof,
306+
start_loc,
307+
&ops,
308+
&chunks,
309+
&root
310+
), "proof with bad prefix peaks should not verify");
311+
}
312+
313+
let bad_suffix_peaks =
314+
bad_suffix_peaks.iter().map(|d| Digest::from(*d)).collect();
315+
if range_proof.unfolded_suffix_peaks != bad_suffix_peaks {
316+
let mut bad_proof = range_proof.clone();
317+
bad_proof.unfolded_suffix_peaks = bad_suffix_peaks;
318+
assert!(!Db::<F>::verify_range_proof(
319+
&mut hasher,
320+
&bad_proof,
321+
start_loc,
322+
&ops,
323+
&chunks,
324+
&root
325+
), "proof with bad suffix peaks should not verify");
326+
}
288327
}
289328
}
290329

storage/src/merkle/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub use position::Position;
3333
#[cfg(test)]
3434
pub(crate) use proof::build_range_proof;
3535
pub use proof::Proof;
36+
#[cfg(feature = "std")]
37+
pub(crate) use proof::{build_range_collection_proof, range_collection_nodes};
3638
pub use read::Readable;
3739
use thiserror::Error;
3840

storage/src/merkle/proof.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,70 @@ impl<F: Family, D: Digest> Proof<F, D> {
738738
)
739739
.ok_or(ReconstructionError::InvalidProof)
740740
}
741+
742+
/// Authenticate the proven range without reconstructing the full generic Merkle root.
743+
///
744+
/// This consumes only the sibling digests needed to rebuild the proven range peaks, then returns
745+
/// those peak digests in `collected`. It deliberately does not consume peak-bagging witnesses
746+
/// such as prefix peaks, after peaks, or backward-fold suffix accumulators.
747+
///
748+
/// Current QMDB grafted proofs use this path because their final root is rebuilt by the wrapper
749+
/// from the collected range peaks plus grafted prefix/suffix witnesses. Including generic
750+
/// peak-bagging witnesses here would create proof bytes that the wrapper root ignores, making
751+
/// those bytes malleable.
752+
pub(crate) fn reconstruct_range_collecting<H, E>(
753+
&self,
754+
hasher: &H,
755+
elements: &[E],
756+
start_loc: Location<F>,
757+
collected: &mut Vec<(Position<F>, D)>,
758+
) -> Result<(), ReconstructionError>
759+
where
760+
H: Hasher<F, Digest = D>,
761+
E: AsRef<[u8]>,
762+
{
763+
if elements.is_empty() {
764+
return Err(ReconstructionError::MissingElements);
765+
}
766+
if !start_loc.is_valid_index() {
767+
return Err(ReconstructionError::InvalidStartLoc);
768+
}
769+
let end_loc = start_loc
770+
.checked_add(elements.len() as u64)
771+
.ok_or(ReconstructionError::InvalidEndLoc)?;
772+
if end_loc > self.leaves {
773+
return Err(ReconstructionError::InvalidEndLoc);
774+
}
775+
776+
let range = start_loc..end_loc;
777+
let bp = Blueprint::new_using_policy(
778+
self.leaves,
779+
self.inactive_peaks,
780+
Bagging::ForwardFold,
781+
range,
782+
)
783+
.map_err(|_| ReconstructionError::InvalidSize)?;
784+
785+
let mut sibling_cursor = 0usize;
786+
let mut elements_iter = elements.iter();
787+
for peak in &bp.range_peaks {
788+
let peak_digest = peak.reconstruct_digest(
789+
hasher,
790+
&bp.range,
791+
&mut elements_iter,
792+
&self.digests,
793+
&mut sibling_cursor,
794+
Some(collected),
795+
)?;
796+
collected.push((peak.pos, peak_digest));
797+
}
798+
799+
if elements_iter.next().is_some() || sibling_cursor != self.digests.len() {
800+
return Err(ReconstructionError::ExtraDigests);
801+
}
802+
803+
Ok(())
804+
}
741805
}
742806

743807
/// A perfect binary subtree within a peak, identified by its root position, height,
@@ -1222,6 +1286,52 @@ where
12221286
)
12231287
}
12241288

1289+
/// Return the node positions needed by [`build_range_collection_proof`].
1290+
#[cfg(feature = "std")]
1291+
pub(crate) fn range_collection_nodes<F: Family>(
1292+
leaves: Location<F>,
1293+
inactive_peaks: usize,
1294+
range: Range<Location<F>>,
1295+
) -> Result<Vec<Position<F>>, super::Error<F>> {
1296+
let bp = Blueprint::new_using_policy(leaves, inactive_peaks, Bagging::ForwardFold, range)?;
1297+
let mut fetch_nodes = Vec::new();
1298+
for peak in &bp.range_peaks {
1299+
peak.collect_siblings(&bp.range, &mut fetch_nodes);
1300+
}
1301+
Ok(fetch_nodes)
1302+
}
1303+
1304+
/// Build a proof containing only the sibling digests needed to authenticate the requested range.
1305+
///
1306+
/// The resulting proof cannot reconstruct a complete generic Merkle root on its own. It is intended
1307+
/// for wrappers that rebuild a custom root from separately supplied peak witnesses, such as current
1308+
/// QMDB grafted proofs.
1309+
#[cfg(feature = "std")]
1310+
pub(crate) fn build_range_collection_proof<F, D, E>(
1311+
leaves: Location<F>,
1312+
inactive_peaks: usize,
1313+
range: Range<Location<F>>,
1314+
get_node: impl Fn(Position<F>) -> Option<D>,
1315+
element_pruned: impl Fn(Position<F>) -> E,
1316+
) -> Result<Proof<F, D>, E>
1317+
where
1318+
F: Family,
1319+
D: Digest,
1320+
E: From<super::Error<F>>,
1321+
{
1322+
let fetch_nodes = range_collection_nodes(leaves, inactive_peaks, range)?;
1323+
let mut digests = Vec::with_capacity(fetch_nodes.len());
1324+
for pos in fetch_nodes {
1325+
digests.push(get_node(pos).ok_or_else(|| element_pruned(pos))?);
1326+
}
1327+
1328+
Ok(Proof {
1329+
leaves,
1330+
inactive_peaks,
1331+
digests,
1332+
})
1333+
}
1334+
12251335
/// Returns the positions of the minimal set of nodes whose digests are required to prove the
12261336
/// inclusion of the elements at the specified `locations`, using the provided root bagging.
12271337
#[cfg(any(feature = "std", test))]

0 commit comments

Comments
 (0)