Skip to main content

metamorphic_log/proof/
mod.rs

1//! Inclusion and consistency proof verification (RFC 6962 / RFC 9162).
2//!
3//! This is the heart of Slice 1 (#327): a *real* verifier that recomputes
4//! Merkle roots from proofs and compares them against the supplied root. The
5//! proof MATH is implemented directly here (a verifier that delegates
6//! verification is not a verifier, per #316/#299) using the standard,
7//! well-tested decomposition from RFC 9162 §2.1.3.2 (inclusion) and §2.1.4.2
8//! (consistency). All node hashing goes through the fixed RFC 6962 scheme in
9//! [`crate::merkle`].
10//!
11//! - [`verify_inclusion`] proves a leaf is committed at a given index in a tree
12//!   of a given size whose head is `root`.
13//! - [`verify_consistency`] proves a tree of `size2` (root `root2`) is an
14//!   append-only extension of a tree of `size1` (root `root1`) — the
15//!   anti-equivocation / tamper-evidence property.
16//!
17//! The lower-level [`root_from_inclusion_proof`] and
18//! [`root_from_consistency_proof`] return the recomputed root(s) for callers
19//! that want to compare against a signed checkpoint themselves.
20
21use crate::error::{Error, Result};
22use crate::merkle::{HASH_LEN, Hash, hash_children};
23
24/// Number of trailing-zero bits in `x`; `64` if `x == 0`.
25#[inline]
26fn trailing_zeros(x: u64) -> u32 {
27    x.trailing_zeros()
28}
29
30/// Bit-length of `x` (position of the highest set bit); `0` if `x == 0`.
31#[inline]
32fn bit_len(x: u64) -> u32 {
33    u64::BITS - x.leading_zeros()
34}
35
36/// Count of set bits in `x`.
37#[inline]
38fn ones_count(x: u64) -> u32 {
39    x.count_ones()
40}
41
42/// Validate that every hash in `proof` (and the implied node size) is exactly
43/// [`HASH_LEN`] bytes, returning a typed error otherwise.
44fn check_hash_len(bytes: &[u8]) -> Result<Hash> {
45    let arr: Hash = bytes.try_into().map_err(|_| Error::InvalidHashLength {
46        got: bytes.len(),
47        want: HASH_LEN,
48    })?;
49    Ok(arr)
50}
51
52/// `innerProofSize` from RFC 9162: the number of proof hashes below the point
53/// where the paths to leaf `index` and leaf `size-1` diverge.
54#[inline]
55fn inner_proof_size(index: u64, size: u64) -> u32 {
56    bit_len(index ^ (size - 1))
57}
58
59/// `decompInclProof`: split an inclusion proof into its `(inner, border)`
60/// component lengths. Their sum is the required proof length.
61#[inline]
62fn decomp_incl_proof(index: u64, size: u64) -> (u32, u32) {
63    let inner = inner_proof_size(index, size);
64    let border = ones_count(index >> inner);
65    (inner, border)
66}
67
68/// `chainInner`: fold the lower `inner` proof hashes into `seed`, choosing left
69/// or right placement by the bits of `index`.
70fn chain_inner(seed: Hash, proof: &[Hash], index: u64) -> Hash {
71    let mut acc = seed;
72    for (i, h) in proof.iter().enumerate() {
73        acc = if (index >> i) & 1 == 0 {
74            hash_children(&acc, h)
75        } else {
76            hash_children(h, &acc)
77        };
78    }
79    acc
80}
81
82/// `chainInnerRight`: like [`chain_inner`] but only folds in hashes that lie to
83/// the *left* of the path (used to recompute the earlier subtree root in a
84/// consistency proof).
85fn chain_inner_right(seed: Hash, proof: &[Hash], index: u64) -> Hash {
86    let mut acc = seed;
87    for (i, h) in proof.iter().enumerate() {
88        if (index >> i) & 1 == 1 {
89            acc = hash_children(h, &acc);
90        }
91    }
92    acc
93}
94
95/// `chainBorderRight`: fold the upper (border) proof hashes, all of which are
96/// left-side subtree hashes.
97fn chain_border_right(seed: Hash, proof: &[Hash]) -> Hash {
98    let mut acc = seed;
99    for h in proof {
100        acc = hash_children(h, &acc);
101    }
102    acc
103}
104
105/// Recompute the Merkle root implied by an inclusion proof.
106///
107/// Returns the root that the `(leaf_hash, index, size, proof)` tuple recomputes
108/// to. Requires `0 <= index < size` and a proof of the exact RFC-mandated
109/// length; otherwise returns a typed [`Error`].
110///
111/// This is the building block of [`verify_inclusion`]; use it directly when you
112/// want to compare the recomputed root against a signed checkpoint yourself.
113pub fn root_from_inclusion_proof(
114    index: u64,
115    size: u64,
116    leaf_hash: &[u8],
117    proof: &[Vec<u8>],
118) -> Result<Hash> {
119    if index >= size {
120        return Err(Error::IndexBeyondSize { index, size });
121    }
122    let leaf = check_hash_len(leaf_hash)?;
123
124    let (inner, border) = decomp_incl_proof(index, size);
125    let want = (inner + border) as usize;
126    if proof.len() != want {
127        return Err(Error::WrongProofSize {
128            got: proof.len(),
129            want,
130        });
131    }
132
133    let nodes: Vec<Hash> = proof
134        .iter()
135        .map(|h| check_hash_len(h))
136        .collect::<Result<_>>()?;
137
138    let (inner_nodes, border_nodes) = nodes.split_at(inner as usize);
139    let res = chain_inner(leaf, inner_nodes, index);
140    Ok(chain_border_right(res, border_nodes))
141}
142
143/// Verify an RFC 6962 / RFC 9162 inclusion proof.
144///
145/// Recomputes the root from `(leaf_hash, index, size, proof)` and checks it
146/// against `root`. On success the log has proven that `leaf_hash` is committed
147/// at `index` in the tree of `size` leaves whose head is `root`.
148///
149/// # Errors
150/// - [`Error::IndexBeyondSize`] if `index >= size`.
151/// - [`Error::InvalidHashLength`] if any hash is not 32 bytes.
152/// - [`Error::WrongProofSize`] if the proof length is wrong for `(index, size)`.
153/// - [`Error::RootMismatch`] if the recomputed root differs from `root`.
154pub fn verify_inclusion(
155    index: u64,
156    size: u64,
157    leaf_hash: &[u8],
158    proof: &[Vec<u8>],
159    root: &[u8],
160) -> Result<()> {
161    let expected = check_hash_len(root)?;
162    let calc = root_from_inclusion_proof(index, size, leaf_hash, proof)?;
163    if calc == expected {
164        Ok(())
165    } else {
166        Err(Error::RootMismatch)
167    }
168}
169
170/// Recompute the newer root (`root2`) implied by a consistency proof, after
171/// verifying the proof is internally consistent with `root1`.
172///
173/// Returns the recomputed `size2` root. Requires `0 < size1 <= size2`.
174///
175/// # Errors
176/// - [`Error::SizeRegression`] if `size2 < size1`.
177/// - [`Error::EmptyTreeConsistency`] if `size1 == 0`.
178/// - [`Error::NonEmptyEqualSizeProof`] if `size1 == size2` but the proof is
179///   non-empty.
180/// - [`Error::WrongProofSize`] / [`Error::InvalidHashLength`] for malformed
181///   proofs.
182/// - [`Error::RootMismatch`] if the proof does not reproduce `root1`.
183pub fn root_from_consistency_proof(
184    size1: u64,
185    size2: u64,
186    proof: &[Vec<u8>],
187    root1: &[u8],
188) -> Result<Hash> {
189    if size2 < size1 {
190        return Err(Error::SizeRegression { size1, size2 });
191    }
192    if size1 == 0 {
193        return Err(Error::EmptyTreeConsistency);
194    }
195    let root1 = check_hash_len(root1)?;
196    if size1 == size2 {
197        if !proof.is_empty() {
198            return Err(Error::NonEmptyEqualSizeProof);
199        }
200        return Ok(root1);
201    }
202    // size1 < size2 from here, so a non-empty proof is required.
203    if proof.is_empty() {
204        return Err(Error::WrongProofSize { got: 0, want: 1 });
205    }
206
207    let (inner_full, border) = decomp_incl_proof(size1 - 1, size2);
208    let shift = trailing_zeros(size1);
209    let inner = inner_full - shift; // shift < inner_full since size1 < size2.
210
211    // The proof includes the root of the sub-tree of size 2^shift, unless
212    // size1 *is* that 2^shift, in which case root1 itself is that seed.
213    let (seed, start): (Hash, usize) = if size1 == (1u64 << shift) {
214        (root1, 0)
215    } else {
216        (check_hash_len(&proof[0])?, 1)
217    };
218
219    let want = start + (inner + border) as usize;
220    if proof.len() != want {
221        return Err(Error::WrongProofSize {
222            got: proof.len(),
223            want,
224        });
225    }
226
227    let nodes: Vec<Hash> = proof[start..]
228        .iter()
229        .map(|h| check_hash_len(h))
230        .collect::<Result<_>>()?;
231    let (inner_nodes, border_nodes) = nodes.split_at(inner as usize);
232
233    // Chaining starts at level `shift`.
234    let mask = (size1 - 1) >> shift;
235
236    // Verify the earlier root reproduces root1.
237    let hash1 = chain_inner_right(seed, inner_nodes, mask);
238    let hash1 = chain_border_right(hash1, border_nodes);
239    if hash1 != root1 {
240        return Err(Error::RootMismatch);
241    }
242
243    // Recompute the newer root.
244    let hash2 = chain_inner(seed, inner_nodes, mask);
245    Ok(chain_border_right(hash2, border_nodes))
246}
247
248/// Verify an RFC 6962 / RFC 9162 consistency proof.
249///
250/// Proves that the tree of `size2` (head `root2`) is an append-only extension
251/// of the tree of `size1` (head `root1`): no earlier entry was modified,
252/// reordered, or removed. This is the anti-equivocation property a monitor
253/// walks across checkpoints.
254///
255/// # Errors
256/// Same conditions as [`root_from_consistency_proof`], plus
257/// [`Error::RootMismatch`] if the recomputed newer root differs from `root2`.
258pub fn verify_consistency(
259    size1: u64,
260    size2: u64,
261    proof: &[Vec<u8>],
262    root1: &[u8],
263    root2: &[u8],
264) -> Result<()> {
265    let expected2 = check_hash_len(root2)?;
266    let calc2 = root_from_consistency_proof(size1, size2, proof, root1)?;
267    if calc2 == expected2 {
268        Ok(())
269    } else {
270        Err(Error::RootMismatch)
271    }
272}