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;