Skip to content

Commit bb50c8b

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

18 files changed

Lines changed: 1157 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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)