lys_core/merkle/proof.rs
1//! Wrapper types for Merkle root hashes and proofs, and the public
2//! verification helpers.
3//!
4//! [`RootHash`], [`InclusionProof`], and [`ConsistencyProof`] hide the
5//! `ct_merkle` generics behind public types parameterised only on SHA-256.
6//! Consumers persist proofs by calling [`InclusionProof::as_bytes`] /
7//! [`ConsistencyProof::as_bytes`] and reconstitute them later via
8//! `try_from_bytes`, with malformed-byte failures mapped to
9//! [`TrustError::MerkleTree`].
10//!
11//! [`verify_inclusion`] re-serializes the candidate leaf with the same
12//! `postcard` helper used by [`super::tree::AppendOnlyTree::append`], so
13//! verification observes the exact bytes that were originally hashed.
14//! [`verify_consistency`] verifies on the new root and compares against the
15//! supplied old root.
16//!
17//! External verification requires no tree access at all: a log publishes
18//! `(root bytes, leaf count)` via [`RootHash::to_parts`] plus proof bytes,
19//! and a third party reconstructs the root with [`RootHash::from_parts`],
20//! rebuilds the proofs with `try_from_bytes`, and runs the verification
21//! helpers against only those published primitives.
22
23use ct_merkle::{ConsistencyProof as CtConsistencyProof, InclusionProof as CtInclusionProof};
24use serde::Serialize;
25use sha2::Sha256;
26
27use crate::error::{TrustError, TrustResult};
28use crate::merkle::leaf::serialize_leaf;
29
30/// Root hash of an [`AppendOnlyTree`] at a particular size.
31///
32/// Wraps `ct_merkle::RootHash<Sha256>`, which carries both the 32-byte
33/// SHA-256 Merkle Tree Hash and the number of leaves. The leaf count is part
34/// of the root because ct-merkle's verifier uses it to size proofs — a bare
35/// 32-byte hash without the count cannot drive verification.
36///
37/// [`AppendOnlyTree`]: super::tree::AppendOnlyTree
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct RootHash {
40 inner: ct_merkle::RootHash<Sha256>,
41}
42
43impl RootHash {
44 /// Wraps a ct-merkle root hash. Internal constructor used by the tree.
45 pub(crate) fn from_inner(inner: ct_merkle::RootHash<Sha256>) -> Self {
46 Self { inner }
47 }
48
49 /// Reconstructs a root hash from its published parts: the 32-byte
50 /// SHA-256 Merkle Tree Hash and the leaf count at which that root was
51 /// computed.
52 ///
53 /// This is the entry point for **external verifiers**. A transparency
54 /// log publishes `(root bytes, leaf count)` — obtained from
55 /// [`Self::to_parts`] — alongside proof bytes; a third party that holds
56 /// only those published primitives reconstructs the root with this
57 /// constructor and then verifies proofs via [`verify_inclusion`] /
58 /// [`verify_consistency`] without ever having access to the tree
59 /// itself.
60 ///
61 /// No validation is possible at construction time — a root hash is an
62 /// opaque commitment. Supplying bytes or a leaf count that do not match
63 /// the tree that produced the proofs causes verification to fail, which
64 /// is exercised by this module's tests.
65 pub fn from_parts(root_hash: [u8; 32], num_leaves: u64) -> Self {
66 let digest = sha2::digest::Output::<Sha256>::from(root_hash);
67 Self {
68 inner: ct_merkle::RootHash::new(digest, num_leaves),
69 }
70 }
71
72 /// Returns the published parts of this root: the 32-byte SHA-256 Merkle
73 /// Tree Hash and the leaf count at which it was computed.
74 ///
75 /// The returned pair is exactly what [`Self::from_parts`] accepts, so a
76 /// log operator publishes these two values and an external verifier
77 /// reconstructs an equal `RootHash` from them.
78 pub fn to_parts(&self) -> ([u8; 32], u64) {
79 ((*self.inner.as_bytes()).into(), self.inner.num_leaves())
80 }
81
82 /// Returns the 32-byte SHA-256 Merkle Tree Hash.
83 pub fn as_bytes(&self) -> &[u8] {
84 self.inner.as_bytes().as_slice()
85 }
86
87 /// Returns the number of leaves in the tree at the time this root was
88 /// computed.
89 pub fn num_leaves(&self) -> u64 {
90 self.inner.num_leaves()
91 }
92}
93
94/// Inclusion proof for a single leaf at a known index.
95///
96/// Produced by [`super::tree::AppendOnlyTree::prove_inclusion`] and verified
97/// by [`verify_inclusion`].
98///
99/// `PartialEq`/`Eq` are intentionally not implemented: ct-merkle's
100/// `InclusionProof<H>` derives them with a `H: Eq` bound that `Sha256` does
101/// not satisfy. Equality testing of proofs is done by comparing the raw
102/// bytes via [`Self::as_bytes`].
103#[derive(Clone, Debug)]
104pub struct InclusionProof {
105 inner: CtInclusionProof<Sha256>,
106}
107
108impl InclusionProof {
109 /// Wraps a ct-merkle inclusion proof. Internal constructor used by the
110 /// tree.
111 pub(crate) fn from_inner(inner: CtInclusionProof<Sha256>) -> Self {
112 Self { inner }
113 }
114
115 /// Returns a reference to the wrapped ct-merkle inclusion proof.
116 pub(crate) fn as_inner(&self) -> &CtInclusionProof<Sha256> {
117 &self.inner
118 }
119
120 /// Returns the RFC 6962 `PATH(m, D[n])` byte encoding of the proof.
121 pub fn as_bytes(&self) -> &[u8] {
122 self.inner.as_bytes()
123 }
124
125 /// Reconstructs an inclusion proof from its byte encoding.
126 ///
127 /// # Errors
128 ///
129 /// Returns [`TrustError::MerkleTree`] if `bytes.len()` is not a multiple
130 /// of the SHA-256 digest size, i.e. the input is not a concatenated
131 /// sequence of valid hash digests.
132 pub fn try_from_bytes(bytes: Vec<u8>) -> TrustResult<Self> {
133 CtInclusionProof::<Sha256>::try_from_bytes(bytes)
134 .map(|inner| Self { inner })
135 .map_err(|e| TrustError::MerkleTree {
136 reason: format!("inclusion proof bytes are malformed: {e}"),
137 })
138 }
139}
140
141/// Consistency proof showing that one tree is a prefix of another.
142///
143/// Produced by [`super::tree::AppendOnlyTree::prove_consistency`] and
144/// verified by [`verify_consistency`].
145#[derive(Clone, Debug)]
146pub struct ConsistencyProof {
147 inner: CtConsistencyProof<Sha256>,
148}
149
150impl ConsistencyProof {
151 /// Wraps a ct-merkle consistency proof. Internal constructor used by the
152 /// tree.
153 pub(crate) fn from_inner(inner: CtConsistencyProof<Sha256>) -> Self {
154 Self { inner }
155 }
156
157 /// Returns a reference to the wrapped ct-merkle consistency proof.
158 pub(crate) fn as_inner(&self) -> &CtConsistencyProof<Sha256> {
159 &self.inner
160 }
161
162 /// Returns the RFC 6962 `PROOF(m, D[n])` byte encoding of the proof.
163 pub fn as_bytes(&self) -> &[u8] {
164 self.inner.as_bytes()
165 }
166
167 /// Reconstructs a consistency proof from its byte encoding.
168 ///
169 /// # Errors
170 ///
171 /// Returns [`TrustError::MerkleTree`] if `bytes.len()` is not a multiple
172 /// of the SHA-256 digest size.
173 pub fn try_from_bytes(bytes: Vec<u8>) -> TrustResult<Self> {
174 CtConsistencyProof::<Sha256>::try_from_bytes(bytes)
175 .map(|inner| Self { inner })
176 .map_err(|e| TrustError::MerkleTree {
177 reason: format!("consistency proof bytes are malformed: {e}"),
178 })
179 }
180}
181
182/// Verifies that `leaf` was at position `index` in the tree whose root is
183/// `root_hash`.
184///
185/// `leaf` is serialized via the same `postcard` helper used by
186/// [`super::tree::AppendOnlyTree::append`], so the verifier hashes the same
187/// bytes that were originally inserted. A mismatched root, a tampered leaf,
188/// or an out-of-range index all fail verification.
189///
190/// # Errors
191///
192/// Returns [`TrustError::MerkleTree`] if:
193/// - serializing `leaf` fails, or
194/// - ct-merkle reports that the proof does not verify against `root_hash` for
195/// the given `index` (including index-out-of-range, empty-tree root,
196/// incorrect-hash, or malformed-proof cases).
197pub fn verify_inclusion<L: Serialize>(
198 root_hash: &RootHash,
199 leaf: &L,
200 index: u64,
201 proof: &InclusionProof,
202) -> TrustResult<()> {
203 let serialized = serialize_leaf(leaf)?;
204 root_hash
205 .inner
206 .verify_inclusion(&serialized, index, proof.as_inner())
207 .map_err(|e| TrustError::MerkleTree {
208 reason: format!("inclusion proof verification failed: {e}"),
209 })
210}
211
212/// Verifies inclusion of RAW leaf bytes (hashed as `SHA-256(0x00 ‖ bytes)`
213/// per RFC 6962), the counterpart of [`verify_inclusion`] for
214/// [`AppendOnlyTree<RawLeaf>`](super::tree::RawLeaf).
215///
216/// The bytes are hashed exactly as supplied — no postcard, no length
217/// prefix — so a third party holding only the raw leaf file recomputes the
218/// leaf hash with any SHA-256 tool and drives this verification from the
219/// published `(root bytes, leaf count)` and proof bytes alone.
220///
221/// # Errors
222///
223/// Returns [`TrustError::MerkleTree`] if ct-merkle reports that the proof
224/// does not verify against `root_hash` for the given `index` (including
225/// index-out-of-range, empty-tree root, incorrect-hash, or malformed-proof
226/// cases).
227pub fn verify_inclusion_raw(
228 root_hash: &RootHash,
229 leaf_bytes: &[u8],
230 index: u64,
231 proof: &InclusionProof,
232) -> TrustResult<()> {
233 root_hash
234 .inner
235 .verify_inclusion(&leaf_bytes, index, proof.as_inner())
236 .map_err(|e| TrustError::MerkleTree {
237 reason: format!("inclusion proof verification failed: {e}"),
238 })
239}
240
241/// Verifies that the tree described by `old_root` is a prefix of the tree
242/// described by `new_root`.
243///
244/// # Errors
245///
246/// Returns [`TrustError::MerkleTree`] if ct-merkle rejects the proof — for
247/// example because the old tree is empty, the old tree is larger than the
248/// new tree, the proof bytes are the wrong length, or the recomputed root
249/// hashes do not match (which is what happens when leaves between the two
250/// trees have been reordered or modified).
251pub fn verify_consistency(
252 old_root: &RootHash,
253 new_root: &RootHash,
254 proof: &ConsistencyProof,
255) -> TrustResult<()> {
256 new_root
257 .inner
258 .verify_consistency(&old_root.inner, proof.as_inner())
259 .map_err(|e| TrustError::MerkleTree {
260 reason: format!("consistency proof verification failed: {e}"),
261 })
262}
263
264#[cfg(test)]
265#[path = "proof_tests.rs"]
266mod tests;