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}