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}