Skip to content

Commit ab3f7a1

Browse files
support split bagging in qmdb-current
1 parent d920b54 commit ab3f7a1

18 files changed

Lines changed: 1156 additions & 441 deletions

File tree

storage/conformance.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,35 @@ hash = "320162dfeef0511822a1c7f4f17535b939beb2dd73e61d639b6300eed2068973"
168168

169169
["commonware_storage::qmdb::conformance::CurrentMmbOrderedFixedConf"]
170170
n_cases = 200
171-
hash = "f265ca1f2f7f571230ef4e576b949d3699b619f108cb0a4e2e28c352f7b32aee"
171+
hash = "c13c9100d657cd01fdf4cff19985bba9e65b7c057391785c5d401a13c976b0af"
172172

173173
["commonware_storage::qmdb::conformance::CurrentMmbOrderedVariableConf"]
174174
n_cases = 200
175-
hash = "74d3a4b1721216ed1bdfc564f5ae07759476b0ceae7dbd1e953c35538a1b0755"
175+
hash = "8fed62bd40fa5ef36f6aebff5152e37d1089e4a66f8dfb9eec8cb00132487efb"
176176

177177
["commonware_storage::qmdb::conformance::CurrentMmbUnorderedFixedConf"]
178178
n_cases = 200
179-
hash = "92b2d71ff19a0f24f337a934d43ee06e3f81a155cd4adc97c58b27cef5cde435"
179+
hash = "81fb100d508803580ae22a96b76955b6480f3526254703c9d75ebb56f0f053f1"
180180

181181
["commonware_storage::qmdb::conformance::CurrentMmbUnorderedVariableConf"]
182182
n_cases = 200
183-
hash = "bad7ded44c7cedc95d202dc3334e3de88877d3aeb63b5808b18bd12c2f59c0d9"
183+
hash = "c46b60791c970b54e936d47a8e2eaae45e9e34c9fb0e1278e678f378392a02f1"
184184

185185
["commonware_storage::qmdb::conformance::CurrentMmrOrderedFixedConf"]
186186
n_cases = 200
187-
hash = "95dc738adeb88d6546c62828e448213fe723d600f57bb4660ecf982ee76e29ef"
187+
hash = "c343e3779b03cb33bba2b8e8fc3e7f4e333bd6dda919bc52bdc6c8385e0fa6dc"
188188

189189
["commonware_storage::qmdb::conformance::CurrentMmrOrderedVariableConf"]
190190
n_cases = 200
191-
hash = "a52a284b8aec813f99234a0545d7c8e651b6f6c93d1912394be3e46075a2681d"
191+
hash = "6cb0dd9ab52acd83bab9d20f69aa0ef18c6e61020ce8ea118601f608f751f6cd"
192192

193193
["commonware_storage::qmdb::conformance::CurrentMmrUnorderedFixedConf"]
194194
n_cases = 200
195-
hash = "537f4c63d903b112fc045ed47f0127da3e2ca70e38c8406e6535a94b2dddc693"
195+
hash = "dfebe6590b0c02f3441ae6f76d5ee11d148839a847884df2f18032c46f1b9fc9"
196196

197197
["commonware_storage::qmdb::conformance::CurrentMmrUnorderedVariableConf"]
198198
n_cases = 200
199-
hash = "93926373351527268aead35fc1884ff27ae2a154df38309dbed50c2c388d4870"
199+
hash = "092416c17d1a0bb69234756ff8cf83e2b4ca8fec2a97c5ca2ecdf20b53b913ef"
200200

201201
["commonware_storage::qmdb::conformance::ImmutableMmbCompactFixedConf"]
202202
n_cases = 200

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 + Bagging>(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 + Bagging>(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 + Bagging>(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 + Bagging>(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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)