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    #[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;