ct_merkle/
consistency.rs

1//! Types and traits for Merkle consistency proofs
2
3use crate::{
4    error::ConsistencyVerifError, 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 one Merkle tree is a prefix of another. In other words, tree #2 is the result of
14/// appending some number of items to the end of tree #1.
15#[derive(Clone, Debug)]
16pub struct ConsistencyProof<H: Digest> {
17    proof: Vec<u8>,
18    _marker: PhantomData<H>,
19}
20
21impl<H: Digest> ConsistencyProof<H> {
22    /// Returns the byte representation of this consistency proof.
23    ///
24    /// This is precisely `PROOF(m, D[n])`, described in [RFC 6962
25    /// §2.1.2](https://www.rfc-editor.org/rfc/rfc6962.html#section-2.1.2), where `n` is the number
26    /// of leaves and `m` is the leaf index being proved.
27    pub fn as_bytes(&self) -> &[u8] {
28        self.proof.as_slice()
29    }
30
31    /// Constructs a `ConsistencyProof` from the given bytes.
32    ///
33    /// # Errors
34    ///
35    /// If when `bytes.len()` is not a multiple of `H::OutputSize::USIZE`, i.e., when `bytes` is not
36    /// a concatenated sequence of hash digests.
37    pub fn try_from_bytes(bytes: Vec<u8>) -> Result<Self, ConsistencyVerifError> {
38        if bytes.len() % H::OutputSize::USIZE != 0 {
39            Err(ConsistencyVerifError::MalformedProof)
40        } else {
41            Ok(ConsistencyProof {
42                proof: bytes,
43                _marker: PhantomData,
44            })
45        }
46    }
47
48    /// Constructs a `ConsistencyProof` from a sequence of digests.
49    // This is identical to `InclusionProof::from_digests`, since proofs are just sequences of
50    // digests.
51    pub fn from_digests<'a>(digests: impl IntoIterator<Item = &'a digest::Output<H>>) -> Self {
52        // The proof is just a concatenation of hashes
53        let concatenated_hashes = digests.into_iter().flatten().cloned().collect();
54
55        ConsistencyProof {
56            proof: concatenated_hashes,
57            _marker: PhantomData,
58        }
59    }
60}
61
62impl<H, T> MemoryBackedTree<H, T>
63where
64    H: Digest,
65    T: HashableLeaf,
66{
67    /// Produces a proof that this `MemoryBackedTree` is the result of appending `num_additions` items
68    /// to a prefix of this tree.
69    ///
70    /// # Panics
71    /// Panics if `num_additions >= self.len()`.
72    pub fn prove_consistency(&self, num_additions: usize) -> ConsistencyProof<H> {
73        let num_leaves = self.len();
74        let num_additions = num_additions as u64;
75        assert!(
76            num_leaves > num_additions,
77            "num_additions must be smaller than self.len()"
78        );
79
80        // This cannot panic because num_leaves > num_additions, and
81        // num_leaves <= ⌊u64::MAX / 2⌋ in any valid MemoryBackedTree
82        let idxs = indices_for_consistency_proof(num_leaves - num_additions, num_additions);
83        // We can unwrap() below because all the given indices are in the tree, which we are storing
84        // in memory
85        let proof = idxs
86            .iter()
87            .flat_map(|&i| &self.internal_nodes[usize::try_from(i).unwrap()])
88            .cloned()
89            .collect();
90        ConsistencyProof {
91            proof,
92            _marker: PhantomData,
93        }
94    }
95}
96
97/// Given a tree size and number of additions, produces a list of tree node indices whose values in
98/// the new tree (i.e., including the additions) are needed to build the consistency proof.
99///
100/// This is useful when we don't have the entire tree in memory, e.g., when it is stored on disk or
101/// stored in tiles on a remote server. Once the digests are retreived, they can be used in the same
102/// order in [`ConsistencyProof::from_digests`].
103///
104/// # Panics
105/// Panics if `num_oldtree_leaves == 0` or `num_oldtree_leaves + num_additions > ⌊u64::MAX / 2⌋ + 1`.
106pub fn indices_for_consistency_proof(num_oldtree_leaves: u64, num_additions: u64) -> Vec<u64> {
107    if num_oldtree_leaves == 0 {
108        panic!("cannot produce a consistency proof starting from an empty tree");
109    }
110    if num_oldtree_leaves
111        .checked_add(num_additions)
112        .is_some_and(|s| s > u64::MAX / 2 + 1)
113    {
114        panic!("too many leaves")
115    }
116
117    let mut out = Vec::new();
118
119    // The root_idx() and LeafIdx::new() calls below cannot panic, because `num_newtree_leaves`, and
120    // hence `num_oldtree_leaves` are guaranteed to be within range from the checks above.
121    let num_newtree_leaves = num_oldtree_leaves + num_additions;
122    let newtree_root_idx = root_idx(num_newtree_leaves);
123    let oldtree_root_idx = root_idx(num_oldtree_leaves);
124
125    // We have starting_idx in a current tree and a old tree. starting_idx occurs in a subtree
126    // which is both a subtree of the current tree and of the old tree.
127    // We want to find the largest such subtree, and start logging the copath after that.
128
129    // We have a special case when the old tree is a subtree of the current tree. This happens
130    // when the old tree is a complete binary tree OR when the old tree equals this tree (i.e.,
131    // nothing changed between the trees).
132    let oldtree_is_subtree =
133        num_oldtree_leaves.is_power_of_two() || num_oldtree_leaves == num_newtree_leaves;
134
135    // If the old tree is a subtree, then the starting idx for the path is the subtree root
136    let mut path_idx = if oldtree_is_subtree {
137        oldtree_root_idx
138    } else {
139        // If the old tree isn't a subtree, find the first place that the ancestors of the
140        // starting index diverge. This cannot panic because `num_newtree_leaves >
141        // num_oldtree_leaves` from the `oldtree_is_subtree` branch above, and `num_oldtree_leaves
142        // != 0` due to the check at the very beginning.
143        let ancestor_in_tree =
144            first_node_with_diverging_parents(num_newtree_leaves, num_oldtree_leaves);
145        // Record the point just before divergences
146        out.push(ancestor_in_tree.as_u64());
147
148        ancestor_in_tree
149    };
150
151    // Now collect the copath, just like in the inclusion proof
152    while path_idx != newtree_root_idx {
153        // The sibling() and parent() computations cannot panic because 1) the computations above
154        // are guaranteed to set path_idx to a valid index in the new tree (and calling .parent() is
155        // a valid transform), and 2) if we're in this loop, then path_idx is not the root yet.
156        let sibling_idx = path_idx.sibling(num_newtree_leaves);
157        out.push(sibling_idx.as_u64());
158
159        // Go up a level
160        path_idx = path_idx.parent(num_newtree_leaves);
161    }
162
163    out
164}
165
166impl<H: Digest> RootHash<H> {
167    /// Verifies a proof that the tree described by `old_root` is a prefix of the tree described by
168    /// `self`. This does not panic.
169    ///
170    /// # Note
171    /// Verification success does NOT imply that the size of the tree that produced the proof equals
172    /// `self.num_leaves()`, or that the old tree size was `old_root.num_leaves()`. For any given
173    /// proof, there are multiple tree size combinations that could have produced it.
174    pub fn verify_consistency(
175        &self,
176        old_root: &RootHash<H>,
177        proof: &ConsistencyProof<H>,
178    ) -> Result<(), ConsistencyVerifError> {
179        let num_newtree_leaves = self.num_leaves;
180        let num_oldtree_leaves = old_root.num_leaves;
181        if num_oldtree_leaves == 0 {
182            return Err(ConsistencyVerifError::OldTreeEmpty);
183        }
184        if num_oldtree_leaves > num_newtree_leaves {
185            return Err(ConsistencyVerifError::OldTreeLarger);
186        }
187        if num_newtree_leaves > u64::MAX / 2 + 1 {
188            return Err(ConsistencyVerifError::NewTreeTooBig);
189        }
190
191        // Check that the proof is the right size
192        // This cannot panic because we check that num_newtree_leaves >= num_old_tree_leaves
193        let num_additions = num_newtree_leaves - num_oldtree_leaves;
194        let expected_proof_size = {
195            // This cannot panic because we check that num_old_tree_leaves > 0 above, and that
196            // `num_oldtree_leaves + num_additions - 1 <= u64::MAX``
197            let num_hashes = indices_for_consistency_proof(num_oldtree_leaves, num_additions).len();
198            H::OutputSize::USIZE * num_hashes
199        };
200        if expected_proof_size != proof.proof.len() {
201            return Err(ConsistencyVerifError::MalformedProof);
202        }
203
204        // We have a special case when the old tree is a subtree of the current tree. This happens
205        // when the old tree is a complete binary tree OR when the old tree is the same size as this
206        // tree
207        let oldtree_is_subtree =
208            old_root.num_leaves.is_power_of_two() || old_root.num_leaves == self.num_leaves;
209
210        // Split the proof into digest-sized chunks
211        let mut digests = proof
212            .proof
213            .chunks(H::OutputSize::USIZE)
214            .map(digest::Output::<H>::from_slice);
215
216        // We compute both old and new tree hashes. This procedure will succeed iff the oldtree
217        // hash matches old_root and the tree hash matches self
218        // The root_idx() cannot panic because `num_oldtree_leaves < num_newtree_leaves` is in range
219        let oldtree_root_idx = root_idx(num_oldtree_leaves);
220        let (mut running_oldtree_idx, mut running_oldtree_hash) = if oldtree_is_subtree {
221            (oldtree_root_idx, old_root.root_hash.clone())
222        } else {
223            // We can unwrap here because the proof size cannot be 0. Proof size is 0 iff the old
224            // root has the same number of leaves as the new one, and that handled in the branche
225            // above
226            let first_hash = digests.next().unwrap().clone();
227            // Our starting point will be a node common to both trees, but whose parents differ
228            // between the two trees.
229            // This cannot panic because `0 < num_oldtree_leaves`, and `num_oldtree_leaves <
230            // num_newtree_leaves` via the `oldtree_is_subtree` check above, and
231            // `num_newtree_leaves` is in range.
232            let starting_idx =
233                first_node_with_diverging_parents(num_newtree_leaves, num_oldtree_leaves);
234            (starting_idx, first_hash)
235        };
236        let mut running_tree_hash = running_oldtree_hash.clone();
237        let mut running_newtree_idx = running_oldtree_idx;
238
239        for sibling_hash in digests {
240            // The sibling(), parent(), and is_left() computations cannot panic because the
241            // computations above are guaranteed to set running_newtree_idx to a valid non-root
242            // index in the new tree (and calling .parent() is a valid transform) not the root yet.
243            let sibling_idx = running_newtree_idx.sibling(num_newtree_leaves);
244
245            if running_newtree_idx.is_left(num_newtree_leaves) {
246                running_tree_hash = parent_hash::<H>(&running_tree_hash, sibling_hash);
247            } else {
248                running_tree_hash = parent_hash::<H>(sibling_hash, &running_tree_hash);
249            }
250            // Step up the tree
251            running_newtree_idx = running_newtree_idx.parent(num_newtree_leaves);
252
253            // Now do the same with the old tree. If the current copath node is the sibling of
254            // running_oldtree_idx, then we can update the oldtree hash
255
256            // We can do the sibling(), is_left(), and parent() computations here for the same
257            // reason as above. Namely, running_oldtree_idx is guaranteed to be a valid index,
258            // .parent() is a valid transform, and the check below ensure it's not the root
259            if running_oldtree_idx != oldtree_root_idx
260                && sibling_idx == running_oldtree_idx.sibling(num_oldtree_leaves)
261            {
262                if running_oldtree_idx.is_left(num_oldtree_leaves) {
263                    running_oldtree_hash = parent_hash::<H>(&running_oldtree_hash, sibling_hash);
264                } else {
265                    running_oldtree_hash = parent_hash::<H>(sibling_hash, &running_oldtree_hash);
266                }
267                // Step up the oldtree
268                running_oldtree_idx = running_oldtree_idx.parent(num_oldtree_leaves);
269            }
270        }
271
272        // At the end, the old hash should be the old root, and the new hash should be the new root
273        let oldtree_eq = running_oldtree_hash.ct_eq(&old_root.root_hash);
274        let tree_eq = running_tree_hash.ct_eq(&self.root_hash);
275        if !bool::from(oldtree_eq & tree_eq) {
276            Err(ConsistencyVerifError::VerificationFailure)
277        } else {
278            Ok(())
279        }
280    }
281}
282
283/// Given two trees `num_leaves1 > num_leaves2`, finds the lowest node in the rightmost path-to-root
284/// of `num_leaves2` whose parent in `num_leaves2` is not the same as the parent in `num_leaves1`.
285/// This is guaranteed to exist as long as `num_leaves2` is not a subtree of `num_leaves1`.
286///
287/// # Panics
288/// Panics when `num_leaves1 <= num_leaves2` or `num_leaves2 == 0`. Also panics when `num_leaves2` is
289/// a subtree of `num_leaves1`, which occurs when `num_leaves2.is_power_of_two()`. Also panics when
290/// `num_leaves1 > ⌊u64::MAX / 2⌋ + 1`.
291fn first_node_with_diverging_parents(num_leaves1: u64, num_leaves2: u64) -> InternalIdx {
292    assert!(num_leaves1 > num_leaves2);
293    assert_ne!(num_leaves2, 0);
294    assert!(num_leaves1 <= u64::MAX / 2 + 1);
295
296    let mut idx = InternalIdx::from(LeafIdx::new(num_leaves2 - 1));
297    while idx.parent(num_leaves1) == idx.parent(num_leaves2) {
298        idx = idx.parent(num_leaves1);
299    }
300
301    idx
302}
303
304#[cfg(test)]
305pub(crate) mod test {
306    use crate::{
307        mem_backed_tree::test::{rand_tree, rand_val},
308        RootHash,
309    };
310    use sha2::Sha256;
311
312    // Tests that an honestly generated consistency proof verifies, and that a valid proof wrt one
313    // or two modified roots does not
314    #[test]
315    fn consistency_proof() {
316        let mut rng = rand::rng();
317
318        for initial_size in 1..25 {
319            for num_to_add in 0..25 {
320                let mut t = rand_tree(&mut rng, initial_size);
321                let initial_root = t.root();
322
323                // Now add to v
324                for _ in 0..num_to_add {
325                    let val = rand_val(&mut rng);
326                    t.push(val);
327                }
328                let new_root = t.root();
329
330                // Now make a consistency proof and check it
331                let proof = t.prove_consistency(num_to_add);
332                new_root
333                    .verify_consistency(&initial_root, &proof)
334                    .unwrap_or_else(|e| {
335                        panic!(
336                            "Consistency check failed for {} -> {} leaves: {}",
337                            initial_size,
338                            initial_size + num_to_add,
339                            e
340                        )
341                    });
342
343                // Make new roots with a different levels and make sure it fails
344                let modified_new_root =
345                    RootHash::<Sha256>::new(new_root.root_hash, new_root.num_leaves() * 2);
346                assert!(
347                    modified_new_root
348                        .verify_consistency(&initial_root, &proof)
349                        .is_err(),
350                    "proof verified wrt modified new root"
351                );
352                let modified_new_root =
353                    RootHash::<Sha256>::new(new_root.root_hash, new_root.num_leaves() / 2);
354                assert!(
355                    modified_new_root
356                        .verify_consistency(&initial_root, &proof)
357                        .is_err(),
358                    "proof verified wrt modified new root"
359                )
360            }
361        }
362    }
363}