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}