Skip to main content

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;