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}