ct_merkle/inclusion.rs
1//! Types and traits for inclusion proofs, a.k.a., Merkle Audit Paths
2
3use crate::{
4 error::InclusionVerifError, mem_backed_tree::MemoryBackedTree, tree_util::*, RootHash,
5};
6
7use alloc::vec::Vec;
8use core::marker::PhantomData;
9
10use digest::{typenum::Unsigned, Digest};
11use subtle::ConstantTimeEq;
12
13/// A proof that a value appears in a Merkle tree
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct InclusionProof<H: Digest> {
16 proof: Vec<u8>,
17 _marker: PhantomData<H>,
18}
19
20impl<H: Digest> InclusionProof<H> {
21 /// Returns the byte representation of this inclusion proof.
22 ///
23 /// This is precisely `PATH(m, D[n])`, described in [RFC 6962
24 /// §2.1.1](https://www.rfc-editor.org/rfc/rfc6962.html#section-2.1.1), where `n` is the number
25 /// of leaves and `m` is the leaf index being proved.
26 pub fn as_bytes(&self) -> &[u8] {
27 self.proof.as_slice()
28 }
29
30 /// Constructs an `InclusionProof` from its serialized form.
31 ///
32 /// # Panics
33 /// Panics when `bytes.len()` is not a multiple of `H::OutputSize::USIZE`, i.e., when `bytes` is
34 /// not a concatenated sequence of hash digests.
35 pub fn from_bytes(bytes: Vec<u8>) -> Self {
36 if bytes.len() % H::OutputSize::USIZE != 0 {
37 panic!("malformed inclusion proof");
38 } else {
39 InclusionProof {
40 proof: bytes,
41 _marker: PhantomData,
42 }
43 }
44 }
45
46 /// Constructs an `InclusionProof` from a sequence of digests.
47 pub fn from_digests<'a>(digests: impl IntoIterator<Item = &'a digest::Output<H>>) -> Self {
48 // The proof is just a concatenation of hashes
49 let concatenated_hashes = digests.into_iter().flatten().cloned().collect();
50
51 InclusionProof {
52 proof: concatenated_hashes,
53 _marker: PhantomData,
54 }
55 }
56
57 /// Constructs a `InclusionProof` from the given bytes.
58 ///
59 /// # Errors
60 ///
61 /// If when `bytes.len()` is not a multiple of `H::OutputSize::USIZE`, i.e., when `bytes`
62 /// is not a concatenated sequence of hash digests.
63 pub fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, InclusionVerifError> {
64 if bytes.len() % H::OutputSize::USIZE != 0 {
65 return Err(InclusionVerifError::MalformedProof);
66 }
67
68 Ok(InclusionProof {
69 proof: bytes,
70 _marker: PhantomData,
71 })
72 }
73}
74
75impl<H, T> MemoryBackedTree<H, T>
76where
77 H: Digest,
78 T: HashableLeaf,
79{
80 /// Returns a proof of inclusion of the item at the given index.
81 ///
82 /// # Panics
83 /// Panics if `idx >= self.len()`.
84 pub fn prove_inclusion(&self, idx: usize) -> InclusionProof<H> {
85 let num_leaves = self.len();
86
87 // Get the indices we need to make the proof
88 let idxs = indices_for_inclusion_proof(num_leaves, idx as u64);
89 // Get the hashes at those indices. We can unwrap below because the indices are guaranteed
90 // to be in the tree, which is stored in memory
91 let sibling_hashes = idxs
92 .iter()
93 .map(|&i| &self.internal_nodes[usize::try_from(i).unwrap()]);
94
95 // Make the proof
96 InclusionProof::from_digests(sibling_hashes)
97 }
98}
99
100/// Given a tree size and index, produces a list of tree node indices whose values we need in order
101/// to build the inclusion proof.
102///
103/// This is useful when we don't have the entire tree in memory, e.g., when it is stored on disk or
104/// stored in tiles on a remote server. Once the digests are retreived, they can be used in the same
105/// order in [`InclusionProof::from_digests`].
106///
107/// # Panics
108/// Panics if `num_leaves == 0`, if `idx >= num_leaves`, or if `idx > ⌊u64::MAX / 2⌋`.
109pub fn indices_for_inclusion_proof(num_leaves: u64, idx: u64) -> Vec<u64> {
110 if num_leaves == 0 {
111 panic!("cannot create an inclusion proof for an empty tree")
112 }
113 if idx >= num_leaves {
114 panic!("cannot create an inclusion proof for an index that's not in the tree")
115 }
116 if idx > u64::MAX / 2 {
117 panic!("leaf index is too high")
118 }
119
120 let mut out = Vec::new();
121 let root_idx = root_idx(num_leaves);
122
123 // If this is the singleton tree, the proof is empty, and we need no values
124 if num_leaves == 1 {
125 return out;
126 }
127
128 // Start the proof with the sibling hash
129 let start_idx = InternalIdx::from(LeafIdx::new(idx));
130 let sibling_idx = start_idx.sibling(num_leaves);
131 out.push(sibling_idx.as_u64());
132
133 // Collect the hashes of the siblings on the way up the tree
134 let mut parent_idx = start_idx.parent(num_leaves);
135 while parent_idx != root_idx {
136 let sibling_idx = parent_idx.sibling(num_leaves);
137 out.push(sibling_idx.as_u64());
138
139 // Go up a level
140 parent_idx = parent_idx.parent(num_leaves);
141 }
142
143 out
144}
145
146impl<H: Digest> RootHash<H> {
147 /// Verifies that `val` occurs at index `idx` in the tree described by this `RootHash`. This
148 /// does not panic.
149 ///
150 /// # Note
151 /// Verification success does NOT imply that the size of the tree that produced the proof equals
152 /// `self.num_leaves()`. For any given proof, there are multiple tree sizes that could have
153 /// produced it.
154 pub fn verify_inclusion<T: HashableLeaf>(
155 &self,
156 val: &T,
157 idx: u64,
158 proof: &InclusionProof<H>,
159 ) -> Result<(), InclusionVerifError> {
160 if idx >= self.num_leaves {
161 return Err(InclusionVerifError::IndexOutOfRange);
162 }
163 if self.num_leaves == 0 {
164 return Err(InclusionVerifError::TreeEmpty);
165 }
166
167 // Check that the proof is the right size. If not, return an error
168 let InclusionProof { proof, .. } = proof;
169 // We can call indices_for_inclusion_proof without panicking because we checked that
170 // idx < self.num_leaves and self.num_leaves != 0 above
171 let expected_proof_size = {
172 let num_hashes = indices_for_inclusion_proof(self.num_leaves, idx).len();
173 H::OutputSize::USIZE * num_hashes
174 };
175 if proof.len() != expected_proof_size {
176 return Err(InclusionVerifError::MalformedProof);
177 }
178
179 // If the proof is empty, then the leaf hash is the root hash
180 let leaf_hash = leaf_hash::<H, _>(val);
181 if proof.is_empty() && leaf_hash.ct_eq(&self.root_hash).into() {
182 return Ok(());
183 }
184
185 // Otherwise, start hashing up the tree
186 let mut cur_idx: InternalIdx = LeafIdx::new(idx).into();
187 let mut cur_hash = leaf_hash;
188 // Invariant: hash_slice is always H::OutputSize::USIZE because we checked above that
189 // proof.len() is the correct length, which calculated as a multiple of the digest len
190 for hash_slice in proof.chunks(H::OutputSize::USIZE) {
191 // Hash the current node with its provided sibling
192 let sibling_hash = digest::Output::<H>::from_slice(hash_slice);
193 if cur_idx.is_left(self.num_leaves) {
194 cur_hash = parent_hash::<H>(&cur_hash, sibling_hash);
195 } else {
196 cur_hash = parent_hash::<H>(sibling_hash, &cur_hash);
197 }
198
199 // Step up the tree
200 // This panics if `cur_idx` is the root index. This cannot happen because have checked
201 // that `idx < self.num_leaves`, and the proof is the correct length, i.e., precisely
202 // long enough for the *final* value of `cur_idx` to be the root index.
203 cur_idx = cur_idx.parent(self.num_leaves);
204 }
205
206 // Now compare the final hash with the root hash
207 if cur_hash.ct_eq(&self.root_hash).into() {
208 Ok(())
209 } else {
210 Err(InclusionVerifError::IncorrectHash)
211 }
212 }
213}
214
215#[cfg(test)]
216pub(crate) mod test {
217 use crate::{mem_backed_tree::test::rand_tree, RootHash};
218
219 // Tests that an honestly generated inclusion proof verifies
220 #[test]
221 fn inclusion_proof_correctness() {
222 let mut rng = rand::rng();
223
224 let t = rand_tree(&mut rng, 100);
225
226 // Check inclusion at every index
227 for idx in 0..t.len() as usize {
228 let proof = t.prove_inclusion(idx);
229 let elem = t.get(idx).unwrap();
230
231 // Now check the proof
232 let root = t.root();
233 root.verify_inclusion(&elem, idx as u64, &proof).unwrap();
234
235 // Make two roots whose tree heights are different from the original. This should
236 // trigger a "proof isn't the correct length" error.
237 let modified_root = RootHash::new(*root.as_bytes(), 2 * root.num_leaves());
238 assert!(modified_root
239 .verify_inclusion(&elem, idx as u64, &proof)
240 .is_err());
241 let modified_root = RootHash::new(*root.as_bytes(), root.num_leaves() / 2);
242 assert!(modified_root
243 .verify_inclusion(&elem, idx as u64, &proof)
244 .is_err());
245 }
246 }
247}