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 #[must_use]
66 pub fn from_parts(root_hash: [u8; 32], num_leaves: u64) -> Self {
67 let digest = sha2::digest::Output::<Sha256>::from(root_hash);
68 Self {
69 inner: ct_merkle::RootHash::new(digest, num_leaves),
70 }
71 }
72
73 /// Returns the published parts of this root: the 32-byte SHA-256 Merkle
74 /// Tree Hash and the leaf count at which it was computed.
75 ///
76 /// The returned pair is exactly what [`Self::from_parts`] accepts, so a
77 /// log operator publishes these two values and an external verifier
78 /// reconstructs an equal `RootHash` from them.
79 #[must_use]
80 pub fn to_parts(&self) -> ([u8; 32], u64) {
81 ((*self.inner.as_bytes()).into(), self.inner.num_leaves())
82 }
83
84 /// Returns the 32-byte SHA-256 Merkle Tree Hash.
85 #[must_use]
86 pub fn as_bytes(&self) -> &[u8] {
87 self.inner.as_bytes().as_slice()
88 }
89
90 /// Returns the number of leaves in the tree at the time this root was
91 /// computed.
92 #[must_use]
93 pub fn num_leaves(&self) -> u64 {
94 self.inner.num_leaves()
95 }
96}
97
98/// Inclusion proof for a single leaf at a known index.
99///
100/// Produced by [`super::tree::AppendOnlyTree::prove_inclusion`] and verified
101/// by [`verify_inclusion`].
102///
103/// `PartialEq`/`Eq` are intentionally not implemented: ct-merkle's
104/// `InclusionProof<H>` derives them with a `H: Eq` bound that `Sha256` does
105/// not satisfy. Equality testing of proofs is done by comparing the raw
106/// bytes via [`Self::as_bytes`].
107#[derive(Clone, Debug)]
108pub struct InclusionProof {
109 inner: CtInclusionProof<Sha256>,
110}
111
112impl InclusionProof {
113 /// Wraps a ct-merkle inclusion proof. Internal constructor used by the
114 /// tree.
115 pub(crate) fn from_inner(inner: CtInclusionProof<Sha256>) -> Self {
116 Self { inner }
117 }
118
119 /// Returns a reference to the wrapped ct-merkle inclusion proof.
120 pub(crate) fn as_inner(&self) -> &CtInclusionProof<Sha256> {
121 &self.inner
122 }
123
124 /// Returns the RFC 6962 `PATH(m, D[n])` byte encoding of the proof.
125 #[must_use]
126 pub fn as_bytes(&self) -> &[u8] {
127 self.inner.as_bytes()
128 }
129
130 /// Reconstructs an inclusion proof from its byte encoding.
131 ///
132 /// # Errors
133 ///
134 /// Returns [`TrustError::MerkleTree`] if `bytes.len()` is not a multiple
135 /// of the SHA-256 digest size, i.e. the input is not a concatenated
136 /// sequence of valid hash digests.
137 pub fn try_from_bytes(bytes: Vec<u8>) -> TrustResult<Self> {
138 CtInclusionProof::<Sha256>::try_from_bytes(bytes)
139 .map(|inner| Self { inner })
140 .map_err(|e| TrustError::MerkleTree {
141 reason: format!("inclusion proof bytes are malformed: {e}"),
142 })
143 }
144}
145
146/// Consistency proof showing that one tree is a prefix of another.
147///
148/// Produced by [`super::tree::AppendOnlyTree::prove_consistency`] and
149/// verified by [`verify_consistency`].
150#[derive(Clone, Debug)]
151pub struct ConsistencyProof {
152 inner: CtConsistencyProof<Sha256>,
153}
154
155impl ConsistencyProof {
156 /// Wraps a ct-merkle consistency proof. Internal constructor used by the
157 /// tree.
158 pub(crate) fn from_inner(inner: CtConsistencyProof<Sha256>) -> Self {
159 Self { inner }
160 }
161
162 /// Returns a reference to the wrapped ct-merkle consistency proof.
163 pub(crate) fn as_inner(&self) -> &CtConsistencyProof<Sha256> {
164 &self.inner
165 }
166
167 /// Returns the RFC 6962 `PROOF(m, D[n])` byte encoding of the proof.
168 #[must_use]
169 pub fn as_bytes(&self) -> &[u8] {
170 self.inner.as_bytes()
171 }
172
173 /// Reconstructs a consistency proof from its byte encoding.
174 ///
175 /// # Errors
176 ///
177 /// Returns [`TrustError::MerkleTree`] if `bytes.len()` is not a multiple
178 /// of the SHA-256 digest size.
179 pub fn try_from_bytes(bytes: Vec<u8>) -> TrustResult<Self> {
180 CtConsistencyProof::<Sha256>::try_from_bytes(bytes)
181 .map(|inner| Self { inner })
182 .map_err(|e| TrustError::MerkleTree {
183 reason: format!("consistency proof bytes are malformed: {e}"),
184 })
185 }
186}
187
188/// Verifies that `leaf` was at position `index` in the tree whose root is
189/// `root_hash`.
190///
191/// `leaf` is serialized via the same `postcard` helper used by
192/// [`super::tree::AppendOnlyTree::append`], so the verifier hashes the same
193/// bytes that were originally inserted. A mismatched root, a tampered leaf,
194/// or an out-of-range index all fail verification.
195///
196/// # Errors
197///
198/// Returns [`TrustError::MerkleTree`] if:
199/// - serializing `leaf` fails, or
200/// - ct-merkle reports that the proof does not verify against `root_hash` for
201/// the given `index` (including index-out-of-range, empty-tree root,
202/// incorrect-hash, or malformed-proof cases).
203pub fn verify_inclusion<L: Serialize>(
204 root_hash: &RootHash,
205 leaf: &L,
206 index: u64,
207 proof: &InclusionProof,
208) -> TrustResult<()> {
209 let serialized = serialize_leaf(leaf)?;
210 root_hash
211 .inner
212 .verify_inclusion(&serialized, index, proof.as_inner())
213 .map_err(|e| TrustError::MerkleTree {
214 reason: format!("inclusion proof verification failed: {e}"),
215 })
216}
217
218/// Verifies inclusion of RAW leaf bytes (hashed as `SHA-256(0x00 ‖ bytes)`
219/// per RFC 6962), the counterpart of [`verify_inclusion`] for
220/// [`AppendOnlyTree<RawLeaf>`](super::tree::RawLeaf).
221///
222/// The bytes are hashed exactly as supplied — no postcard, no length
223/// prefix — so a third party holding only the raw leaf file recomputes the
224/// leaf hash with any SHA-256 tool and drives this verification from the
225/// published `(root bytes, leaf count)` and proof bytes alone.
226///
227/// # Errors
228///
229/// Returns [`TrustError::MerkleTree`] if ct-merkle reports that the proof
230/// does not verify against `root_hash` for the given `index` (including
231/// index-out-of-range, empty-tree root, incorrect-hash, or malformed-proof
232/// cases).
233pub fn verify_inclusion_raw(
234 root_hash: &RootHash,
235 leaf_bytes: &[u8],
236 index: u64,
237 proof: &InclusionProof,
238) -> TrustResult<()> {
239 root_hash
240 .inner
241 .verify_inclusion(&leaf_bytes, index, proof.as_inner())
242 .map_err(|e| TrustError::MerkleTree {
243 reason: format!("inclusion proof verification failed: {e}"),
244 })
245}
246
247/// Verifies that the tree described by `old_root` is a prefix of the tree
248/// described by `new_root`.
249///
250/// # Errors
251///
252/// Returns [`TrustError::MerkleTree`] if ct-merkle rejects the proof — for
253/// example because the old tree is empty, the old tree is larger than the
254/// new tree, the proof bytes are the wrong length, or the recomputed root
255/// hashes do not match (which is what happens when leaves between the two
256/// trees have been reordered or modified).
257pub fn verify_consistency(
258 old_root: &RootHash,
259 new_root: &RootHash,
260 proof: &ConsistencyProof,
261) -> TrustResult<()> {
262 new_root
263 .inner
264 .verify_consistency(&old_root.inner, proof.as_inner())
265 .map_err(|e| TrustError::MerkleTree {
266 reason: format!("consistency proof verification failed: {e}"),
267 })
268}
269
270#[cfg(test)]
271#[path = "proof_tests.rs"]
272mod tests;