Skip to main content

commonware_storage/mmr/
verification.rs

1//! Defines functions for generating various [Proof] types from any MMR implementing the [Storage]
2//! trait. Also defines a [ProofStore] type that can be used to generate proofs over any subset or
3//! 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 MMR state
9//! - [historical_range_proof] generates proofs against historical MMR states
10//!
11//! Historical proofs are essential for sync operations where we need to prove elements against a
12//! past state of the MMR rather than its current state.
13
14use crate::mmr::{hasher::Hasher, proof, storage::Storage, Error, Location, Position, Proof};
15use commonware_cryptography::Digest;
16use core::ops::Range;
17use futures::future::try_join_all;
18use std::collections::{BTreeSet, HashMap};
19
20/// A store derived from a [Proof] that can be used to generate proofs over any sub-range of the
21/// original range.
22pub struct ProofStore<D> {
23    digests: HashMap<Position, D>,
24    size: Position,
25}
26
27impl<D: Digest> ProofStore<D> {
28    /// Create a new [ProofStore] from a valid [Proof] over the given range of elements. The
29    /// resulting store can be used to generate proofs over any sub-range of the original range.
30    /// Returns an error if the proof is invalid or could not be verified against the given root.
31    pub fn new<H, E>(
32        hasher: &mut H,
33        proof: &Proof<D>,
34        elements: &[E],
35        start_loc: Location,
36        root: &D,
37    ) -> Result<Self, Error>
38    where
39        H: Hasher<Digest = D>,
40        E: AsRef<[u8]>,
41    {
42        let digests =
43            proof.verify_range_inclusion_and_extract_digests(hasher, elements, start_loc, root)?;
44
45        Self::new_from_digests(proof.leaves, digests)
46    }
47
48    /// Create a new [ProofStore] from the result of calling
49    /// [Proof::verify_range_inclusion_and_extract_digests]. The resulting store can be used to
50    /// generate proofs over any sub-range of the original range.
51    pub fn new_from_digests(leaves: Location, digests: Vec<(Position, D)>) -> Result<Self, Error> {
52        Ok(Self {
53            size: Position::try_from(leaves)?,
54            digests: digests.into_iter().collect(),
55        })
56    }
57
58    /// Return a range proof for the nodes corresponding to the given location range.
59    pub async fn range_proof(&self, range: Range<Location>) -> Result<Proof<D>, Error> {
60        range_proof(self, range).await
61    }
62
63    /// Return a multi proof for the elements corresponding to the given locations.
64    pub async fn multi_proof(&self, locations: &[Location]) -> Result<Proof<D>, Error> {
65        multi_proof(self, locations).await
66    }
67}
68
69impl<D: Digest> Storage<D> for ProofStore<D> {
70    async fn get_node(&self, pos: Position) -> Result<Option<D>, Error> {
71        Ok(self.digests.get(&pos).cloned())
72    }
73
74    fn size(&self) -> Position {
75        self.size
76    }
77}
78
79/// Return a range proof for the nodes corresponding to the given location range.
80///
81/// # Errors
82///
83/// Returns [Error::LocationOverflow] if any location in `range` > [crate::mmr::MAX_LOCATION]
84/// Returns [Error::RangeOutOfBounds] if any location in `range` > `mmr.size()`
85/// Returns [Error::ElementPruned] if some element needed to generate the proof has been pruned
86/// Returns [Error::Empty] if the requested range is empty
87pub async fn range_proof<D: Digest, S: Storage<D>>(
88    mmr: &S,
89    range: Range<Location>,
90) -> Result<Proof<D>, Error> {
91    let leaves = Location::try_from(mmr.size())?;
92    historical_range_proof(mmr, leaves, range).await
93}
94
95/// Analogous to range_proof but for a previous database state. Specifically, the state when the MMR
96/// had `leaves` leaves.
97///
98/// # Errors
99///
100/// Returns [Error::LocationOverflow] if any location in `range` > [crate::mmr::MAX_LOCATION]
101/// Returns [Error::RangeOutOfBounds] if any location in `range` > `leaves`
102/// Returns [Error::ElementPruned] if some element needed to generate the proof has been pruned
103/// Returns [Error::Empty] if the requested range is empty
104pub async fn historical_range_proof<D: Digest, S: Storage<D>>(
105    mmr: &S,
106    leaves: Location,
107    range: Range<Location>,
108) -> Result<Proof<D>, Error> {
109    // Get the positions of all nodes needed to generate the proof.
110    let positions = proof::nodes_required_for_range_proof(leaves, range)?;
111
112    // Fetch the digest of each.
113    let mut digests: Vec<D> = Vec::new();
114    let node_futures = positions.iter().map(|pos| mmr.get_node(*pos));
115    let hash_results = try_join_all(node_futures).await?;
116
117    for (i, hash_result) in hash_results.into_iter().enumerate() {
118        match hash_result {
119            Some(hash) => digests.push(hash),
120            None => return Err(Error::ElementPruned(positions[i])),
121        };
122    }
123
124    Ok(Proof { leaves, digests })
125}
126
127/// Return an inclusion proof for the elements at the specified locations. This is analogous to
128/// range_proof but supports non-contiguous locations.
129///
130/// The order of positions does not affect the output (sorted internally).
131///
132/// # Errors
133///
134/// Returns [Error::LocationOverflow] if any location in `locations` > [crate::mmr::MAX_LOCATION]
135/// Returns [Error::RangeOutOfBounds] if any location in `locations` > `mmr.size()`
136/// Returns [Error::ElementPruned] if some element needed to generate the proof has been pruned
137/// Returns [Error::Empty] if locations is empty
138pub async fn multi_proof<D: Digest, S: Storage<D>>(
139    mmr: &S,
140    locations: &[Location],
141) -> Result<Proof<D>, Error> {
142    if locations.is_empty() {
143        // Disallow proofs over empty element lists just as we disallow proofs over empty ranges.
144        return Err(Error::Empty);
145    }
146
147    // Collect all required node positions
148    let size = mmr.size();
149    let leaves = Location::try_from(size)?;
150    let node_positions: BTreeSet<_> = proof::nodes_required_for_multi_proof(leaves, locations)?;
151
152    // Fetch all required digests in parallel and collect with positions
153    let node_futures: Vec<_> = node_positions
154        .iter()
155        .map(|&pos| async move { mmr.get_node(pos).await.map(|digest| (pos, digest)) })
156        .collect();
157    let results = try_join_all(node_futures).await?;
158
159    // Build the proof, returning error with correct position on pruned nodes
160    let mut digests = Vec::with_capacity(results.len());
161    for (pos, digest) in results {
162        match digest {
163            Some(digest) => digests.push(digest),
164            None => return Err(Error::ElementPruned(pos)),
165        }
166    }
167
168    Ok(Proof { leaves, digests })
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::mmr::{location::LocationRangeExt as _, mem::DirtyMmr, StandardHasher as Standard};
175    use commonware_cryptography::{sha256::Digest, Hasher, Sha256};
176    use commonware_macros::test_traced;
177    use commonware_runtime::{deterministic, Runner};
178
179    fn test_digest(v: u8) -> Digest {
180        Sha256::hash(&[v])
181    }
182
183    #[test_traced]
184    fn test_verification_proof_store() {
185        let executor = deterministic::Runner::default();
186        executor.start(|_| async move {
187            // create a new MMR and add a non-trivial amount (49) of elements
188            let mut mmr = DirtyMmr::new();
189            let mut elements = Vec::new();
190            let mut element_positions = Vec::new();
191            let mut hasher: Standard<Sha256> = Standard::new();
192            for i in 0..49 {
193                elements.push(test_digest(i));
194                element_positions.push(mmr.add(&mut hasher, elements.last().unwrap()));
195            }
196            let mmr = mmr.merkleize(&mut hasher, None);
197            let root = mmr.root();
198
199            // Extract a ProofStore from a proof over a variety of ranges, starting with the full
200            // range and shrinking each endpoint with each iteration.
201            let mut range_start = Location::new_unchecked(0);
202            let mut range_end = Location::new_unchecked(49);
203            while range_start < range_end {
204                let range = range_start..range_end;
205                let range_proof = mmr.range_proof(range.clone()).unwrap();
206                let proof_store = ProofStore::new(
207                    &mut hasher,
208                    &range_proof,
209                    &elements[range.to_usize_range()],
210                    range_start,
211                    root,
212                )
213                .unwrap();
214
215                // Verify that the ProofStore can be used to generate proofs over a host of sub-ranges
216                // starting with the full range down to a range containing a single element.
217                let mut subrange_start = range_start;
218                let mut subrange_end = range_end;
219                while subrange_start < subrange_end {
220                    // Verify a proof over a sub-range of the original range.
221                    let sub_range = subrange_start..subrange_end;
222                    let sub_range_proof = proof_store.range_proof(sub_range.clone()).await.unwrap();
223                    assert!(sub_range_proof.verify_range_inclusion(
224                        &mut hasher,
225                        &elements[sub_range.to_usize_range()],
226                        sub_range.start,
227                        root
228                    ));
229                    subrange_start += 1;
230                    subrange_end -= 1;
231                }
232                range_start += 1;
233                range_end -= 1;
234            }
235        });
236    }
237}