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