Skip to main content

commonware_storage/merkle/
verification.rs

1//! Defines functions for generating various [Proof] types from any Merkle structure implementing
2//! the [Storage] trait. Also defines a [ProofStore] type that can be used to generate proofs over
3//! any subset or sub-range of the original range.
4//!
5//! ## Historical Proof Generation
6//!
7//! This module provides both current and historical proof generation capabilities:
8//! - [range_proof] generates proofs against the current state
9//! - [historical_range_proof] generates proofs against historical states
10//!
11//! Historical proofs are essential for sync operations where we need to prove elements against a
12//! past state of the structure rather than its current state.
13
14use crate::merkle::{
15    hasher::Hasher,
16    proof::{self as merkle_proof, Blueprint},
17    storage::Storage,
18    Error, Family, Location, Position, Proof,
19};
20use commonware_cryptography::Digest;
21use core::ops::Range;
22use futures::future::try_join_all;
23use std::collections::{BTreeSet, HashMap};
24
25/// A store derived from a [Proof] that can be used to generate proofs over any sub-range of the
26/// original range.
27pub struct ProofStore<F: Family, D> {
28    digests: HashMap<Position<F>, D>,
29    size: Position<F>,
30    /// The fold prefix accumulator from the original proof, if any peaks preceded the proven range.
31    fold_acc: Option<D>,
32    /// Number of peaks that were folded into `fold_acc`.
33    num_fold_peaks: usize,
34}
35
36impl<F: Family, D: Digest> ProofStore<F, D> {
37    /// Create a [ProofStore] from a [Proof] of inclusion of the provided range of elements from
38    /// the structure with root `root`. The resulting store can be used to generate range proofs
39    /// over any sub-range of the original range. Returns an error if the proof is invalid or could
40    /// not be verified against the given root.
41    ///
42    /// The fold prefix accumulator from the proof is stored internally so that sub-range proofs
43    /// with different fold prefix boundaries can be generated without requiring individual peak
44    /// digests.
45    pub fn new<H, E>(
46        hasher: &H,
47        proof: &Proof<F, D>,
48        elements: &[E],
49        start_loc: Location<F>,
50        root: &D,
51    ) -> Result<Self, Error<F>>
52    where
53        H: Hasher<F, Digest = D>,
54        E: AsRef<[u8]>,
55    {
56        let digests =
57            proof.verify_range_inclusion_and_extract_digests(hasher, elements, start_loc, root)?;
58        let map: HashMap<Position<F>, D> = digests.into_iter().collect();
59
60        let size = Position::try_from(proof.leaves)?;
61
62        // Count peaks in the fold prefix using the same leaf-coverage logic that proof
63        // construction uses. Some families (for example MMB) do not order peaks by position.
64        let num_fold_peaks = Blueprint::<F>::fold_prefix(proof.leaves, start_loc)?.len();
65
66        let fold_acc = if num_fold_peaks > 0 {
67            Some(*proof.digests.first().ok_or(Error::InvalidProof)?)
68        } else {
69            None
70        };
71
72        Ok(Self {
73            size,
74            digests: map,
75            fold_acc,
76            num_fold_peaks,
77        })
78    }
79
80    /// Return a range proof for the nodes corresponding to the given location range.
81    ///
82    /// The sub-range's fold prefix accumulator is derived from the stored fold accumulator
83    /// (covering the original proof's fold prefix peaks) plus any additional peaks that are
84    /// individually available in the store (original range peaks now preceding the sub-range).
85    pub fn range_proof<H: Hasher<F, Digest = D>>(
86        &self,
87        hasher: &H,
88        range: Range<Location<F>>,
89    ) -> Result<Proof<F, D>, Error<F>> {
90        let leaves = Location::try_from(self.size)?;
91        let bp = Blueprint::new(leaves, range)?;
92
93        let mut digests: Vec<D> = Vec::new();
94        if !bp.fold_prefix.is_empty() {
95            // Start from the stored fold accumulator (which does not include the leaf count).
96            let mut acc = self.fold_acc;
97            // Fold in peaks beyond those already covered by the stored accumulator.
98            for &pos in bp.fold_prefix.iter().skip(self.num_fold_peaks) {
99                match self.digests.get(&pos) {
100                    Some(d) => {
101                        acc = Some(acc.map_or(*d, |a| hasher.fold(&a, d)));
102                    }
103                    None => return Err(Error::ElementPruned(pos)),
104                }
105            }
106            digests.push(acc.expect("fold_prefix is non-empty so acc must be set"));
107        }
108
109        for &pos in &bp.fetch_nodes {
110            match self.digests.get(&pos) {
111                Some(d) => digests.push(*d),
112                None => return Err(Error::ElementPruned(pos)),
113            }
114        }
115
116        Ok(Proof { leaves, digests })
117    }
118
119    /// Return a multi proof for the elements corresponding to the given locations.
120    ///
121    /// Since multi-proofs require individual node digests (not fold accumulators), callers must
122    /// supply any peak digests that fall in the fold prefix of the original proof. These are the
123    /// peaks entirely before the original range's start location. If the original range started
124    /// at location 0, no peaks are needed.
125    pub fn multi_proof(
126        &self,
127        locations: &[Location<F>],
128        peaks: &[(Position<F>, D)],
129    ) -> Result<Proof<F, D>, Error<F>> {
130        if locations.is_empty() {
131            return Err(Error::Empty);
132        }
133
134        let leaves = Location::try_from(self.size)?;
135        let node_positions: BTreeSet<_> =
136            merkle_proof::nodes_required_for_multi_proof(leaves, locations)?;
137
138        let peak_map: HashMap<Position<F>, D> = peaks.iter().copied().collect();
139
140        let mut digests = Vec::with_capacity(node_positions.len());
141        for &pos in &node_positions {
142            if let Some(d) = self.digests.get(&pos) {
143                digests.push(*d);
144            } else if let Some(d) = peak_map.get(&pos) {
145                digests.push(*d);
146            } else {
147                return Err(Error::ElementPruned(pos));
148            }
149        }
150
151        Ok(Proof { leaves, digests })
152    }
153}
154
155/// Return a range proof for the nodes corresponding to the given location range.
156///
157/// # Errors
158///
159/// Returns [Error::LocationOverflow] if any location in `range` > [Family::MAX_LEAVES]
160/// Returns [Error::RangeOutOfBounds] if any location in `range` > `merkle.size()`
161/// Returns [Error::ElementPruned] if some element needed to generate the proof has been pruned
162/// Returns [Error::Empty] if the requested range is empty
163pub async fn range_proof<
164    F: Family,
165    D: Digest,
166    H: Hasher<F, Digest = D>,
167    S: Storage<F, Digest = D>,
168>(
169    hasher: &H,
170    merkle: &S,
171    range: Range<Location<F>>,
172) -> Result<Proof<F, D>, Error<F>> {
173    let leaves = Location::try_from(merkle.size().await)?;
174    historical_range_proof(hasher, merkle, leaves, range).await
175}
176
177/// Analogous to range_proof but for a previous database state. Specifically, the state when the
178/// structure had `leaves` leaves.
179///
180/// # Errors
181///
182/// Returns [Error::LocationOverflow] if any location in `range` > [Family::MAX_LEAVES]
183/// Returns [Error::RangeOutOfBounds] if any location in `range` > `leaves`
184/// Returns [Error::ElementPruned] if some element needed to generate the proof has been pruned
185/// Returns [Error::Empty] if the requested range is empty
186pub async fn historical_range_proof<
187    F: Family,
188    D: Digest,
189    H: Hasher<F, Digest = D>,
190    S: Storage<F, Digest = D>,
191>(
192    hasher: &H,
193    merkle: &S,
194    leaves: Location<F>,
195    range: Range<Location<F>>,
196) -> Result<Proof<F, D>, Error<F>> {
197    let bp = Blueprint::new(leaves, range)?;
198
199    let mut digests: Vec<D> = Vec::new();
200    if !bp.fold_prefix.is_empty() {
201        let node_futures = bp.fold_prefix.iter().map(|&pos| merkle.get_node(pos));
202        let results = try_join_all(node_futures).await?;
203        let mut acc = results[0].ok_or(Error::ElementPruned(bp.fold_prefix[0]))?;
204        for (i, &result) in results.iter().enumerate().skip(1) {
205            let d = result.ok_or(Error::ElementPruned(bp.fold_prefix[i]))?;
206            acc = hasher.fold(&acc, &d);
207        }
208        digests.push(acc);
209    }
210
211    let node_futures = bp.fetch_nodes.iter().map(|&pos| merkle.get_node(pos));
212    let results = try_join_all(node_futures).await?;
213    for (i, result) in results.into_iter().enumerate() {
214        match result {
215            Some(d) => digests.push(d),
216            None => return Err(Error::ElementPruned(bp.fetch_nodes[i])),
217        }
218    }
219
220    Ok(Proof { leaves, digests })
221}
222
223/// Return an inclusion proof for the elements at the specified locations. This is analogous to
224/// range_proof but supports non-contiguous locations.
225///
226/// The order of positions does not affect the output (sorted internally).
227///
228/// # Errors
229///
230/// Returns [Error::LocationOverflow] if any location in `locations` > [Family::MAX_LEAVES]
231/// Returns [Error::RangeOutOfBounds] if any location in `locations` > `merkle.size()`
232/// Returns [Error::ElementPruned] if some element needed to generate the proof has been pruned
233/// Returns [Error::Empty] if locations is empty
234pub async fn multi_proof<F: Family, D: Digest, S: Storage<F, Digest = D>>(
235    merkle: &S,
236    locations: &[Location<F>],
237) -> Result<Proof<F, D>, Error<F>> {
238    if locations.is_empty() {
239        // Disallow proofs over empty element lists just as we disallow proofs over empty ranges.
240        return Err(Error::Empty);
241    }
242
243    // Collect all required node positions
244    let size = merkle.size().await;
245    let leaves = Location::try_from(size)?;
246    let node_positions: BTreeSet<_> =
247        merkle_proof::nodes_required_for_multi_proof(leaves, locations)?;
248
249    // Fetch all required digests in parallel and collect with positions
250    let node_futures: Vec<_> = node_positions
251        .iter()
252        .map(|&pos| async move { merkle.get_node(pos).await.map(|digest| (pos, digest)) })
253        .collect();
254    let results = try_join_all(node_futures).await?;
255
256    // Build the proof, returning error with correct position on pruned nodes
257    let mut digests = Vec::with_capacity(results.len());
258    for (pos, digest) in results {
259        match digest {
260            Some(digest) => digests.push(digest),
261            None => return Err(Error::ElementPruned(pos)),
262        }
263    }
264
265    Ok(Proof { leaves, digests })
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::{
272        merkle::LocationRangeExt as _,
273        mmb::{mem::Mmb, Location as MmbLocation},
274        mmr::{mem::Mmr, StandardHasher as Standard},
275    };
276    use commonware_cryptography::{sha256::Digest, Hasher, Sha256};
277    use commonware_macros::test_traced;
278    use commonware_runtime::{deterministic, Runner};
279
280    fn test_digest(v: u8) -> Digest {
281        Sha256::hash(&[v])
282    }
283
284    #[test_traced]
285    fn test_verification_proof_store() {
286        let executor = deterministic::Runner::default();
287        executor.start(|_| async move {
288            // create a new MMR and add a non-trivial amount (49) of elements
289            let hasher: Standard<Sha256> = Standard::new();
290            let mut mmr = Mmr::new(&hasher);
291            let elements: Vec<_> = (0..49).map(test_digest).collect();
292            let batch = {
293                let mut batch = mmr.new_batch();
294                for element in &elements {
295                    batch = batch.add(&hasher, element);
296                }
297                batch.merkleize(&mmr, &hasher)
298            };
299            mmr.apply_batch(&batch).unwrap();
300            let root = mmr.root();
301
302            // Extract a ProofStore from a proof over a variety of ranges, starting with the full
303            // range and shrinking each endpoint with each iteration.
304            let mut range_start = Location::new(0);
305            let mut range_end = Location::new(49);
306            while range_start < range_end {
307                let range = range_start..range_end;
308                let range_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
309                let proof_store = ProofStore::new(
310                    &hasher,
311                    &range_proof,
312                    &elements[range.to_usize_range()],
313                    range_start,
314                    root,
315                )
316                .unwrap();
317
318                // Verify that the ProofStore can be used to generate proofs over a host of
319                // sub-ranges starting with the full range down to a range containing a single
320                // element.
321                let mut subrange_start = range_start;
322                let mut subrange_end = range_end;
323                while subrange_start < subrange_end {
324                    // Verify a proof over a sub-range of the original range.
325                    let sub_range = subrange_start..subrange_end;
326                    let sub_range_proof =
327                        proof_store.range_proof(&hasher, sub_range.clone()).unwrap();
328                    assert!(sub_range_proof.verify_range_inclusion(
329                        &hasher,
330                        &elements[sub_range.to_usize_range()],
331                        sub_range.start,
332                        root
333                    ));
334                    subrange_start += 1;
335                    subrange_end -= 1;
336                }
337                range_start += 1;
338                range_end -= 1;
339            }
340        });
341    }
342
343    #[test_traced]
344    fn test_verification_proof_store_with_fold_prefix() {
345        let executor = deterministic::Runner::default();
346        executor.start(|_| async move {
347            // Build MMR with 49 elements. Peaks cover locations 0-31, 32-47, 48.
348            // A proof starting at location 32 puts the first peak entirely in the fold prefix.
349            let hasher: Standard<Sha256> = Standard::new();
350            let mut mmr = Mmr::new(&hasher);
351            let elements: Vec<_> = (0..49).map(test_digest).collect();
352            let batch = {
353                let mut batch = mmr.new_batch();
354                for element in &elements {
355                    batch = batch.add(&hasher, element);
356                }
357                batch.merkleize(&mmr, &hasher)
358            };
359            mmr.apply_batch(&batch).unwrap();
360            let root = mmr.root();
361
362            // Proof for range 32..49 has a non-empty fold prefix (the 32-leaf peak).
363            // The ProofStore derives the fold accumulator from the proof itself, so
364            // sub-proofs should succeed for all sub-ranges without needing peaks.
365            let range = Location::new(32)..Location::new(49);
366            let range_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
367            let proof_store = ProofStore::new(
368                &hasher,
369                &range_proof,
370                &elements[range.to_usize_range()],
371                range.start,
372                root,
373            )
374            .unwrap();
375
376            // Sub-proofs should succeed for all sub-ranges.
377            for start in 32u64..49 {
378                for end in (start + 1)..=49 {
379                    let sub_range = Location::new(start)..Location::new(end);
380                    let sub_proof = proof_store.range_proof(&hasher, sub_range.clone()).unwrap();
381                    assert!(
382                        sub_proof.verify_range_inclusion(
383                            &hasher,
384                            &elements[sub_range.to_usize_range()],
385                            sub_range.start,
386                            root,
387                        ),
388                        "sub-proof should verify for range {start}..{end}"
389                    );
390                }
391            }
392        });
393    }
394
395    #[test_traced]
396    fn test_verification_proof_store_with_fold_prefix_mmb() {
397        let executor = deterministic::Runner::default();
398        executor.start(|_| async move {
399            let hasher: Standard<Sha256> = Standard::new();
400            let mut mmb = Mmb::new(&hasher);
401            let elements: Vec<_> = (0..8).map(test_digest).collect();
402            let batch = {
403                let mut batch = mmb.new_batch();
404                for element in &elements {
405                    batch = batch.add(&hasher, element);
406                }
407                batch.merkleize(&mmb, &hasher)
408            };
409            mmb.apply_batch(&batch).unwrap();
410            let root = mmb.root();
411
412            // With 8 leaves, the oldest MMB peak covers locations 0..4 but sits at position 7,
413            // while the first leaf in the proven range (location 4) sits at position 6.
414            // A position-based peak comparison therefore misclassifies the fold prefix.
415            let range = MmbLocation::new(4)..MmbLocation::new(8);
416            let range_proof = mmb.range_proof(&hasher, range.clone()).unwrap();
417            let proof_store = ProofStore::new(
418                &hasher,
419                &range_proof,
420                &elements[range.to_usize_range()],
421                range.start,
422                root,
423            )
424            .unwrap();
425
426            for start in 4u64..8 {
427                for end in (start + 1)..=8 {
428                    let sub_range = MmbLocation::new(start)..MmbLocation::new(end);
429                    let sub_proof = proof_store.range_proof(&hasher, sub_range.clone()).unwrap();
430                    assert!(
431                        sub_proof.verify_range_inclusion(
432                            &hasher,
433                            &elements[sub_range.to_usize_range()],
434                            sub_range.start,
435                            root,
436                        ),
437                        "sub-proof should verify for MMB range {start}..{end}"
438                    );
439                }
440            }
441        });
442    }
443}