Skip to main content

lys_core/merkle/
tree.rs

1//! [`AppendOnlyTree`] — the append-only Merkle log over a SHA-256
2//! `ct_merkle::MemoryBackedTree`.
3//!
4//! Only [`AppendOnlyTree::append`] mutates the tree. There is no API that
5//! deletes, replaces, reorders, or otherwise touches past leaves; the type
6//! exists precisely to make the append-only invariant a property of the
7//! public surface, not just of the underlying RFC 6962 construction.
8//!
9//! ct-merkle's `prove_inclusion` and `prove_consistency` panic on
10//! out-of-range arguments. The wrapper methods pre-check the arguments and
11//! return [`TrustError::MerkleTree`] instead, keeping the no-panic invariant
12//! of this crate intact.
13//!
14//! The tree is generic over the leaf type `L: Serialize`. Leaves are
15//! converted to a deterministic byte representation via `serialize_leaf`
16//! before being pushed into the underlying ct-merkle tree, so the original
17//! `L` is not stored on the tree and the trust crate stays domain-agnostic.
18//!
19//! The [`RawLeaf`] marker selects the parallel raw-byte encoding:
20//! `AppendOnlyTree<RawLeaf>` hashes leaf bytes verbatim
21//! (`SHA-256(0x00 ‖ bytes)` per RFC 6962) via [`AppendOnlyTree::append_raw`]
22//! and never gains the postcard methods, so the two leaf encodings cannot
23//! be mixed in one tree.
24//!
25//! [`HashTree`] is the raw-leaf tree without the leaves: it holds only the
26//! RFC 6962 leaf and interior hashes, is fed leaf hashes rather than bytes,
27//! and proves inclusion and consistency byte-identically to
28//! `AppendOnlyTree<RawLeaf>` over the same leaves. It is the tree for a log
29//! whose leaves live in storage: its memory is at most two hashes per leaf,
30//! and no leaf is hashed by it.
31
32use std::fmt;
33use std::marker::PhantomData;
34
35use ct_merkle::mem_backed_tree::MemoryBackedTree;
36use serde::Serialize;
37use sha2::{Digest, Sha256};
38
39use crate::error::{TrustError, TrustResult};
40use crate::merkle::leaf::{SerializedLeaf, serialize_leaf};
41use crate::merkle::proof::{ConsistencyProof, InclusionProof, RootHash};
42
43mod validation;
44
45use validation::{check_consistency_sizes, check_inclusion_index};
46
47/// Append-only Merkle transparency log over any `L: Serialize` leaf.
48///
49/// Each [`Self::append`] serializes the leaf with `postcard` and pushes the
50/// resulting bytes into a `ct_merkle::MemoryBackedTree<Sha256, _>`. The
51/// original `L` is not stored; the tree retains the serialized bytes plus
52/// the internal Merkle nodes.
53///
54/// The empty tree's root is RFC 6962's deterministic zero-leaf root —
55/// `SHA-256("")` paired with `num_leaves = 0`. After `n` appends, [`Self::root`]
56/// returns the SHA-256 Merkle Tree Hash of the `n` leaves with `num_leaves =
57/// n`. Two trees built from the same `Serialize` sequence in the same order
58/// produce the same root.
59///
60/// # The leaf encoding is a FROZEN WIRE CONTRACT
61///
62/// Leaves are hashed via their `postcard` encoding, which is derived
63/// entirely from `L`'s shape: fields, field declaration order, and enum
64/// variant declaration order. Once leaves of `L` exist in a persisted or
65/// published tree, that shape must **never** change — reordering or adding
66/// fields, or reordering enum variants, silently alters the bytes of
67/// historical leaves, so every previously published root and proof stops
68/// verifying with no error and no version signal. Schema evolution
69/// requires a new versioned leaf type or an explicitly versioned envelope.
70/// When long-lived verifiability matters, prefer a leaf type that pins the
71/// payload as pre-encoded bytes (e.g. a struct holding `Vec<u8>`) so the
72/// hashed bytes are under explicit consumer control. See the
73/// [`leaf`](super::leaf) module docs for the full contract.
74pub struct AppendOnlyTree<L> {
75    inner: MemoryBackedTree<Sha256, SerializedLeaf>,
76    // `fn(L)` is the standard "leaf-type tag" marker — it tracks `L` in the
77    // type signature without imposing variance constraints on the wrapper
78    // and without requiring `L: Debug`/`Clone` on the wrapper's derives.
79    _marker: PhantomData<fn(L)>,
80}
81
82impl<L> AppendOnlyTree<L> {
83    /// Builds a new empty tree.
84    ///
85    /// The empty tree's [`Self::root`] is the deterministic zero-leaf root
86    /// (`SHA-256("")` with `num_leaves = 0`) defined by RFC 6962 and
87    /// implemented by ct-merkle.
88    #[must_use]
89    pub fn new() -> Self {
90        Self {
91            inner: MemoryBackedTree::new(),
92            _marker: PhantomData,
93        }
94    }
95
96    /// Returns the number of leaves in the tree.
97    #[must_use]
98    pub fn len(&self) -> u64 {
99        self.inner.len()
100    }
101
102    /// Returns `true` if no leaves have been appended.
103    #[must_use]
104    pub fn is_empty(&self) -> bool {
105        self.inner.is_empty()
106    }
107
108    /// Returns the current root hash, capturing both the SHA-256 Merkle Tree
109    /// Hash and the current leaf count.
110    #[must_use]
111    pub fn root(&self) -> RootHash {
112        RootHash::from_inner(self.inner.root())
113    }
114
115    /// Builds an inclusion proof for the leaf at `leaf_index`.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`TrustError::MerkleTree`] if `leaf_index` is greater than or
120    /// equal to the current tree length, or if the index does not fit in a
121    /// `usize` on this target. Both conditions are pre-checked so the
122    /// underlying ct-merkle call (which would otherwise panic) is never made
123    /// with an invalid argument.
124    pub fn prove_inclusion(&self, leaf_index: u64) -> TrustResult<InclusionProof> {
125        check_inclusion_index(self.inner.len(), leaf_index)?;
126        let idx = usize::try_from(leaf_index).map_err(|_err| TrustError::MerkleTree {
127            reason: format!("inclusion proof leaf index {leaf_index} does not fit in usize"),
128        })?;
129        Ok(InclusionProof::from_inner(self.inner.prove_inclusion(idx)))
130    }
131
132    /// Builds a consistency proof showing that the tree of size `old_size` is
133    /// a prefix of the current tree (which must be exactly `new_size` leaves).
134    ///
135    /// # Errors
136    ///
137    /// Returns [`TrustError::MerkleTree`] if `new_size` does not equal the
138    /// current tree length, `old_size` is zero, `old_size` exceeds `new_size`,
139    /// or the resulting `num_additions` does not fit in a `usize`. These
140    /// checks ensure the underlying ct-merkle call (which would otherwise
141    /// panic when `num_additions >= self.len()`) is never made with an
142    /// invalid argument.
143    pub fn prove_consistency(&self, old_size: u64, new_size: u64) -> TrustResult<ConsistencyProof> {
144        check_consistency_sizes(self.inner.len(), old_size, new_size)?;
145        // old_size > 0 and old_size <= new_size = self.len(), so this never
146        // wraps around.
147        let num_additions = new_size - old_size;
148        let num_additions_usize =
149            usize::try_from(num_additions).map_err(|_err| TrustError::MerkleTree {
150                reason: format!(
151                    "consistency proof num_additions {num_additions} does not fit in usize"
152                ),
153            })?;
154        Ok(ConsistencyProof::from_inner(
155            self.inner.prove_consistency(num_additions_usize),
156        ))
157    }
158}
159
160/// Type-level marker for trees whose leaves are raw bytes hashed verbatim
161/// (leaf hash = `SHA-256(0x00 ‖ bytes)` per RFC 6962).
162///
163/// Uninhabited: never a value, and it deliberately does NOT implement
164/// `Serialize`, so the postcard methods ([`AppendOnlyTree::append`],
165/// [`AppendOnlyTree::reconstruct_from_leaves`]) do not exist on
166/// `AppendOnlyTree<RawLeaf>` — the two leaf encodings cannot be mixed in
167/// one tree even by accident.
168///
169/// **Invariant:** for every leaf appended via
170/// [`AppendOnlyTree::append_raw`],
171/// `leaf_hash = SHA-256(0x00 ‖ leaf-bytes)`; a third party reproduces it
172/// with `(printf '\x00'; cat leaf-file) | shasum -a 256`.
173pub enum RawLeaf {}
174
175impl AppendOnlyTree<RawLeaf> {
176    /// Appends raw bytes verbatim and returns the new tree size.
177    ///
178    /// The bytes are hashed exactly as supplied — no postcard, no length
179    /// prefix — so the RFC 6962 leaf hash is `SHA-256(0x00 ‖ leaf_bytes)`
180    /// (see [`raw_leaf_hash`](super::leaf::raw_leaf_hash)). Infallible:
181    /// there is no serialization step to fail.
182    pub fn append_raw(&mut self, leaf_bytes: &[u8]) -> u64 {
183        self.inner
184            .push(SerializedLeaf::from_raw_bytes(leaf_bytes.to_vec()));
185        self.inner.len()
186    }
187
188    /// Rebuilds a raw-leaf tree from leaves in their original append order.
189    ///
190    /// The reconstruction path for restart and prefix rebuilds, mirroring
191    /// [`AppendOnlyTree::reconstruct_from_leaves`] for the raw encoding.
192    /// Because it reuses [`Self::append_raw`], the rebuilt tree reproduces
193    /// the original root bit-for-bit.
194    pub fn reconstruct_from_raw_leaves<I>(leaves: I) -> Self
195    where
196        I: IntoIterator,
197        I::Item: AsRef<[u8]>,
198    {
199        let mut tree = Self::new();
200        for leaf in leaves {
201            tree.append_raw(leaf.as_ref());
202        }
203        tree
204    }
205}
206
207impl<L: Serialize> AppendOnlyTree<L> {
208    /// Appends `leaf` to the tree and returns the new tree size.
209    ///
210    /// The leaf is serialized via `postcard` before being hashed into the
211    /// tree. The first append returns `1`, the next `2`, and so on.
212    ///
213    /// # Errors
214    ///
215    /// Returns [`TrustError::MerkleTree`] if the leaf cannot be serialized.
216    /// No other failure modes exist at this scale of tree (ct-merkle's
217    /// internal limits are well beyond practical use).
218    pub fn append(&mut self, leaf: L) -> TrustResult<u64> {
219        let serialized = serialize_leaf(&leaf)?;
220        self.inner.push(serialized);
221        Ok(self.inner.len())
222    }
223
224    /// Rebuilds a tree from a sequence of leaves in their original append
225    /// order.
226    ///
227    /// This is the mechanism for reconstructing in-memory tree state from
228    /// persistent storage on restart. Because reconstruction reuses
229    /// [`Self::append`], it goes through the same serialization path and
230    /// therefore reproduces the original root hash bit-for-bit.
231    ///
232    /// # Errors
233    ///
234    /// Returns [`TrustError::MerkleTree`] if any leaf fails to serialize.
235    pub fn reconstruct_from_leaves(leaves: Vec<L>) -> TrustResult<Self> {
236        let mut tree = Self::new();
237        for leaf in leaves {
238            tree.append(leaf)?;
239        }
240        Ok(tree)
241    }
242}
243
244impl<L> Default for AppendOnlyTree<L> {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250impl<L> fmt::Debug for AppendOnlyTree<L> {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        f.debug_struct("AppendOnlyTree")
253            .field("num_leaves", &self.inner.len())
254            .finish()
255    }
256}
257
258/// RFC 6962 interior-node domain byte.
259const NODE_PREFIX: u8 = 0x01;
260
261/// RFC 6962 interior node: `SHA-256(0x01 ‖ left ‖ right)`.
262fn node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
263    let mut hasher = Sha256::new();
264    hasher.update([NODE_PREFIX]);
265    hasher.update(left);
266    hasher.update(right);
267    hasher.finalize().into()
268}
269
270/// RFC 6962's split of `n >= 2` leaves: the largest power of two below `n`.
271fn split_point(size: u64) -> u64 {
272    1 << (u64::BITS - 1 - (size - 1).leading_zeros())
273}
274
275/// An append-only RFC 6962 tree that holds hashes and nothing else.
276///
277/// It is fed leaf hashes, `SHA-256(0x00 ‖ leaf-bytes)` computed by whoever
278/// read the leaf, and never sees a leaf's bytes: a log builds it by streaming
279/// its stored leaves through, one at a time, and extends it on append with the
280/// leaf hash it already computed, so no leaf is hashed twice.
281///
282/// # Invariants
283///
284/// - Level `h` holds the root of every complete perfect subtree of height `h`,
285///   left to right: entry `i` is the hash of leaves `i·2^h .. (i+1)·2^h`.
286///   Level 0 is the leaf hashes. So a tree of `n` leaves holds
287///   `2n − popcount(n)` hashes, fewer than two per leaf.
288/// - Every RFC 6962 subtree `MTH(D[a:b])` is either one of those perfect
289///   subtrees or the hash of a perfect left subtree and a smaller right one,
290///   so roots and proofs are computed from the stored hashes alone, and are
291///   byte-identical to a raw-leaf [`AppendOnlyTree`]'s over the same leaves.
292/// - Nothing is removed or replaced: [`Self::push_leaf_hash`] is the only
293///   mutation.
294#[derive(Clone, Default)]
295pub struct HashTree {
296    size: u64,
297    levels: Vec<Vec<[u8; 32]>>,
298}
299
300impl HashTree {
301    /// Builds a new empty tree.
302    #[must_use]
303    pub fn new() -> Self {
304        Self::default()
305    }
306
307    /// Builds an empty tree with room for `leaves` leaves and their interior
308    /// hashes, so a tree streamed to a known size allocates once per level and
309    /// holds no spare capacity. A size beyond this target's address space
310    /// reserves nothing and the tree grows as it is fed.
311    #[must_use]
312    pub fn with_capacity(leaves: u64) -> Self {
313        let mut levels = Vec::new();
314        let mut width = leaves;
315        while width > 0 {
316            levels.push(Vec::with_capacity(
317                usize::try_from(width).unwrap_or_default(),
318            ));
319            width >>= 1;
320        }
321        Self { size: 0, levels }
322    }
323
324    /// Returns the number of leaves in the tree.
325    #[must_use]
326    pub fn len(&self) -> u64 {
327        self.size
328    }
329
330    /// Returns `true` if no leaf has been appended.
331    #[must_use]
332    pub fn is_empty(&self) -> bool {
333        self.size == 0
334    }
335
336    /// The bytes of hash storage the tree holds, spare capacity included.
337    /// Never more than two 32-byte hashes per leaf for a tree built by
338    /// [`Self::with_capacity`] to its size.
339    #[must_use]
340    pub fn retained_bytes(&self) -> usize {
341        self.levels
342            .iter()
343            .map(|level| level.capacity() * std::mem::size_of::<[u8; 32]>())
344            .sum()
345    }
346
347    /// Appends a leaf by its RFC 6962 leaf hash and returns the new tree size.
348    ///
349    /// The new leaf completes one perfect subtree per trailing set bit of the
350    /// old size; each is hashed once, here, and kept.
351    pub fn push_leaf_hash(&mut self, leaf_hash: [u8; 32]) -> u64 {
352        let mut node = leaf_hash;
353        let mut height = 0;
354        loop {
355            if self.levels.len() == height {
356                self.levels.push(Vec::new());
357            }
358            let Some(level) = self.levels.get_mut(height) else {
359                break;
360            };
361            level.push(node);
362            if level.len() % 2 == 1 {
363                break;
364            }
365            let [.., left, right] = level.as_slice() else {
366                break;
367            };
368            node = node_hash(left, right);
369            height += 1;
370        }
371        self.size = self.size.saturating_add(1);
372        self.size
373    }
374
375    /// Returns the current root hash with the current leaf count.
376    ///
377    /// # Errors
378    ///
379    /// Returns [`TrustError::MerkleTree`] if a hash the tree must hold is
380    /// missing, which only a broken invariant can cause.
381    pub fn root(&self) -> TrustResult<RootHash> {
382        let root = if self.size == 0 {
383            Sha256::digest([]).into()
384        } else {
385            self.subtree(0, self.size)?
386        };
387        Ok(RootHash::from_parts(root, self.size))
388    }
389
390    /// Builds the RFC 6962 inclusion proof `PATH(leaf_index, D[n])`.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`TrustError::MerkleTree`] if `leaf_index` is not below the
395    /// tree length, with the same reason [`AppendOnlyTree::prove_inclusion`]
396    /// gives.
397    pub fn prove_inclusion(&self, leaf_index: u64) -> TrustResult<InclusionProof> {
398        check_inclusion_index(self.size, leaf_index)?;
399        let mut path = Vec::new();
400        let (mut start, mut size, mut index) = (0, self.size, leaf_index);
401        while size > 1 {
402            let split = split_point(size);
403            if index < split {
404                path.push(self.subtree(start + split, size - split)?);
405                size = split;
406            } else {
407                path.push(self.subtree(start, split)?);
408                start += split;
409                index -= split;
410                size -= split;
411            }
412        }
413        InclusionProof::try_from_bytes(path.iter().rev().flatten().copied().collect())
414    }
415
416    /// Builds the RFC 6962 consistency proof `PROOF(old_size, D[new_size])`.
417    ///
418    /// # Errors
419    ///
420    /// Returns [`TrustError::MerkleTree`] unless `0 < old_size <= new_size`
421    /// and `new_size` is the tree length, with the same reasons
422    /// [`AppendOnlyTree::prove_consistency`] gives.
423    pub fn prove_consistency(&self, old_size: u64, new_size: u64) -> TrustResult<ConsistencyProof> {
424        check_consistency_sizes(self.size, old_size, new_size)?;
425        let mut path = Vec::new();
426        let (mut start, mut size, mut old, mut whole) = (0, new_size, old_size, true);
427        while old != size {
428            let split = split_point(size);
429            if old <= split {
430                path.push(self.subtree(start + split, size - split)?);
431                size = split;
432            } else {
433                path.push(self.subtree(start, split)?);
434                start += split;
435                old -= split;
436                size -= split;
437                whole = false;
438            }
439        }
440        if !whole {
441            path.push(self.subtree(start, size)?);
442        }
443        ConsistencyProof::try_from_bytes(path.iter().rev().flatten().copied().collect())
444    }
445
446    /// `MTH(D[start : start + size])` for a subtree RFC 6962's recursion
447    /// reaches, whose `start` is a multiple of the largest power of two not
448    /// above `size`.
449    fn subtree(&self, start: u64, size: u64) -> TrustResult<[u8; 32]> {
450        if size.is_power_of_two() {
451            let height = size.trailing_zeros();
452            let node = if start % size == 0 {
453                self.stored(height, start >> height)
454            } else {
455                None
456            };
457            return node.ok_or_else(|| TrustError::MerkleTree {
458                reason: format!(
459                    "hash tree of {} leaves holds no node for leaves {start}..{}",
460                    self.size,
461                    start.saturating_add(size)
462                ),
463            });
464        }
465        if size == 0 {
466            return Err(TrustError::MerkleTree {
467                reason: format!("hash tree asked for an empty subtree at leaf {start}"),
468            });
469        }
470        let split = split_point(size);
471        let left = self.subtree(start, split)?;
472        let right = self.subtree(start + split, size - split)?;
473        Ok(node_hash(&left, &right))
474    }
475
476    /// The stored root of the `index`-th perfect subtree of height `height`.
477    fn stored(&self, height: u32, index: u64) -> Option<[u8; 32]> {
478        let level = self.levels.get(usize::try_from(height).ok()?)?;
479        level.get(usize::try_from(index).ok()?).copied()
480    }
481}
482
483impl fmt::Debug for HashTree {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        f.debug_struct("HashTree")
486            .field("num_leaves", &self.size)
487            .finish_non_exhaustive()
488    }
489}
490
491#[cfg(test)]
492#[path = "tree_tests.rs"]
493mod tests;
494
495#[cfg(test)]
496#[path = "hash_tree_tests.rs"]
497mod hash_tree_tests;