Skip to content

Commit 1cb30af

Browse files
support split bagging in qmdb-current
1 parent 4a0cbd4 commit 1cb30af

15 files changed

Lines changed: 1146 additions & 430 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
@@ -54,6 +54,8 @@ enum CurrentOperation {
5454
bad_digests: Vec<[u8; 32]>,
5555
max_ops: NonZeroU64,
5656
bad_chunks: Vec<[u8; 32]>,
57+
bad_prefix_peaks: Vec<[u8; 32]>,
58+
bad_suffix_peaks: Vec<[u8; 32]>,
5759
},
5860
GetSpan {
5961
key: RawKey,
@@ -270,7 +272,14 @@ fn fuzz_family<F: Graftable + Bagging>(data: &FuzzInput, suffix: &str) {
270272
}
271273
}
272274

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

storage/fuzz/fuzz_targets/current_unordered_operations.rs

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

@@ -245,7 +247,14 @@ fn fuzz_family<F: Graftable + Bagging>(data: &FuzzInput, suffix: &str) {
245247
}
246248
}
247249

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

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
@@ -592,6 +592,74 @@ impl<F: Family, D: Digest> Proof<F, D> {
592592
)
593593
.ok_or(ReconstructionError::InvalidProof)
594594
}
595+
596+
/// Authenticate the proven range without reconstructing the full generic Merkle root.
597+
///
598+
/// This consumes only the sibling digests needed to rebuild the proven range peaks, then returns
599+
/// those peak digests in `collected`. It deliberately does not consume peak-bagging witnesses
600+
/// such as prefix peaks, after peaks, or backward-fold suffix accumulators.
601+
///
602+
/// Current QMDB grafted proofs use this path because their final root is rebuilt by the wrapper
603+
/// from the collected range peaks plus grafted prefix/suffix witnesses. Including generic
604+
/// peak-bagging witnesses here would create proof bytes that the wrapper root ignores, making
605+
/// those bytes malleable.
606+
#[cfg(feature = "std")]
607+
pub(crate) fn reconstruct_range_collecting<H, E>(
608+
&self,
609+
hasher: &H,
610+
elements: &[E],
611+
start_loc: Location<F>,
612+
collected: &mut Vec<(Position<F>, D)>,
613+
) -> Result<(), ReconstructionError>
614+
where
615+
H: Hasher<F, Digest = D>,
616+
E: AsRef<[u8]>,
617+
{
618+
if elements.is_empty() {
619+
return Err(ReconstructionError::MissingElements);
620+
}
621+
if !start_loc.is_valid_index() {
622+
return Err(ReconstructionError::InvalidStartLoc);
623+
}
624+
let end_loc = start_loc
625+
.checked_add(elements.len() as u64)
626+
.ok_or(ReconstructionError::InvalidEndLoc)?;
627+
if end_loc > self.leaves {
628+
return Err(ReconstructionError::InvalidEndLoc);
629+
}
630+
631+
let range = start_loc..end_loc;
632+
// Bagging only governs the prefix/suffix accumulator layout, which this method
633+
// deliberately ignores; `range_peaks` and `fetch_nodes` are bagging-independent. Pass
634+
// ForwardFold as a placeholder.
635+
let bp = Blueprint::new(
636+
self.leaves,
637+
self.inactive_peaks,
638+
Bagging::ForwardFold,
639+
range,
640+
)
641+
.map_err(|_| ReconstructionError::InvalidSize)?;
642+
643+
let mut sibling_cursor = 0usize;
644+
let mut elements_iter = elements.iter();
645+
for peak in &bp.range_peaks {
646+
let peak_digest = peak.reconstruct_digest(
647+
hasher,
648+
&bp.range,
649+
&mut elements_iter,
650+
&self.digests,
651+
&mut sibling_cursor,
652+
None,
653+
)?;
654+
collected.push((peak.pos, peak_digest));
655+
}
656+
657+
if elements_iter.next().is_some() || sibling_cursor != self.digests.len() {
658+
return Err(ReconstructionError::ExtraDigests);
659+
}
660+
661+
Ok(())
662+
}
595663
}
596664

597665
/// A perfect binary subtree within a peak, identified by its root position, height,
@@ -1066,6 +1134,56 @@ where
10661134
)
10671135
}
10681136

1137+
/// Return the node positions needed by [`build_range_collection_proof`].
1138+
///
1139+
/// Bagging only governs prefix/suffix accumulator layout, which this helper does not consult; the
1140+
/// `range_peaks` siblings it collects are bagging-independent. ForwardFold is passed as a
1141+
/// placeholder.
1142+
#[cfg(feature = "std")]
1143+
pub(crate) fn range_collection_nodes<F: Family>(
1144+
leaves: Location<F>,
1145+
inactive_peaks: usize,
1146+
range: Range<Location<F>>,
1147+
) -> Result<Vec<Position<F>>, super::Error<F>> {
1148+
let bp = Blueprint::new(leaves, inactive_peaks, Bagging::ForwardFold, range)?;
1149+
let mut fetch_nodes = Vec::new();
1150+
for peak in &bp.range_peaks {
1151+
peak.collect_siblings(&bp.range, &mut fetch_nodes);
1152+
}
1153+
Ok(fetch_nodes)
1154+
}
1155+
1156+
/// Build a proof containing only the sibling digests needed to authenticate the requested range.
1157+
///
1158+
/// The resulting proof cannot reconstruct a complete generic Merkle root on its own. It is intended
1159+
/// for wrappers that rebuild a custom root from separately supplied peak witnesses, such as current
1160+
/// QMDB grafted proofs.
1161+
#[cfg(feature = "std")]
1162+
pub(crate) fn build_range_collection_proof<F, D, E>(
1163+
leaves: Location<F>,
1164+
inactive_peaks: usize,
1165+
range: Range<Location<F>>,
1166+
get_node: impl Fn(Position<F>) -> Option<D>,
1167+
element_pruned: impl Fn(Position<F>) -> E,
1168+
) -> Result<Proof<F, D>, E>
1169+
where
1170+
F: Family,
1171+
D: Digest,
1172+
E: From<super::Error<F>>,
1173+
{
1174+
let fetch_nodes = range_collection_nodes(leaves, inactive_peaks, range)?;
1175+
let mut digests = Vec::with_capacity(fetch_nodes.len());
1176+
for pos in fetch_nodes {
1177+
digests.push(get_node(pos).ok_or_else(|| element_pruned(pos))?);
1178+
}
1179+
1180+
Ok(Proof {
1181+
leaves,
1182+
inactive_peaks,
1183+
digests,
1184+
})
1185+
}
1186+
10691187
/// Returns the positions of the minimal set of nodes whose digests are required to prove the
10701188
/// inclusion of the elements at the specified `locations`, using the provided root bagging.
10711189
#[cfg(any(feature = "std", test))]

0 commit comments

Comments
 (0)