Skip to main content

confium_transparency/
merkle.rs

1//! Merkle tree implementation for transparency log.
2//!
3//! Uses SHA-256 with byte `0x01` prefix for leaf hashing and `0x02`
4//! prefix for internal node hashing (RFC 6962-style domain separation).
5//!
6//! Inclusion proofs include direction bits per RFC 6962 §2.1.1.
7
8use crate::entry::MerkleEntry;
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12/// 32-byte SHA-256 hash.
13pub type Hash = [u8; 32];
14
15/// Which side the proof sibling sits on relative to the current hash.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Side {
19    /// Sibling is to the LEFT of the current hash; combined as `H(sibling || current)`.
20    Left,
21    /// Sibling is to the RIGHT of the current hash; combined as `H(current || sibling)`.
22    Right,
23}
24
25/// A single step in an inclusion proof.
26#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
27pub struct ProofStep {
28    /// Sibling hash.
29    pub sibling: Hash,
30    /// Side of the sibling.
31    pub side: Side,
32}
33
34/// A complete inclusion proof: list of (sibling_hash, side) pairs.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct InclusionProof {
37    /// Sequence number of the leaf being proven.
38    pub sequence: u64,
39    /// Steps from leaf level up to (but not including) the root.
40    pub steps: Vec<ProofStep>,
41}
42
43/// The Merkle tree.
44#[derive(Debug, Default, Clone)]
45pub struct MerkleTree {
46    /// All entries in append order.
47    entries: Vec<MerkleEntry>,
48    /// Cached leaf hashes (level 0 of the tree).
49    leaf_hashes: Vec<Hash>,
50    /// Cached intermediate levels of the tree.
51    /// `levels[0]` is leaf hashes (same as `leaf_hashes`); `levels[k]`
52    /// is the k-th level above the leaves. The last entry is the
53    /// singleton level holding just the root.
54    /// Empty when the tree is empty; rebuilt on every `append`.
55    levels: Vec<Vec<Hash>>,
56    /// Cached root hash. Recomputed on every append; `root()` is O(1).
57    /// `[0u8; 32]` for an empty tree.
58    cached_root: Hash,
59}
60
61/// Errors during Merkle tree operations.
62#[derive(Debug, thiserror::Error)]
63pub enum MerkleError {
64    /// Sequence number out of range.
65    #[error("sequence {0} out of range (have {1} entries)")]
66    OutOfRange(u64, usize),
67    /// Consistency proof failed.
68    #[error("consistency proof failed: expected {expected:?}, got {actual:?}")]
69    ConsistencyFailed {
70        /// Expected root hash.
71        expected: Hash,
72        /// Actual computed root hash.
73        actual: Hash,
74    },
75    /// Inclusion proof failed.
76    #[error("inclusion proof failed for sequence {0}")]
77    InclusionFailed(u64),
78}
79
80fn hash_leaf(entry_hash: Hash) -> Hash {
81    let mut h = Sha256::new();
82    h.update([0x01]);
83    h.update(entry_hash);
84    let r = h.finalize();
85    let mut out = [0u8; 32];
86    out.copy_from_slice(&r);
87    out
88}
89
90fn hash_internal(left: Hash, right: Hash) -> Hash {
91    let mut h = Sha256::new();
92    h.update([0x02]);
93    h.update(left);
94    h.update(right);
95    let r = h.finalize();
96    let mut out = [0u8; 32];
97    out.copy_from_slice(&r);
98    out
99}
100
101/// Largest power of two strictly less than `n`. Returns 0 for `n <= 1`.
102///
103/// Used by [`MerkleTree::consistency_rec`] to split the implicit tree
104/// into LEFT (size `k`) and RIGHT (size `n - k`) subtrees.
105fn largest_pow2_strictly_less_than(n: usize) -> usize {
106    if n <= 1 {
107        return 0;
108    }
109    let mut k = 1usize;
110    while k * 2 < n {
111        k *= 2;
112    }
113    k
114}
115
116impl MerkleTree {
117    /// Construct a new empty tree.
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Append an entry.
123    ///
124    /// O(log N): updates only the path from the new leaf up to the
125    /// root. Each level either pairs its last two nodes (replacing the
126    /// promoted copy one level up) or promotes the new node as-is.
127    pub fn append(&mut self, mut entry: MerkleEntry) -> u64 {
128        if entry.sequence == 0 && !self.entries.is_empty() {
129            entry.sequence = self.entries.len() as u64;
130        }
131        let hash = entry.entry_hash();
132        let leaf = hash_leaf(hash);
133        self.leaf_hashes.push(leaf);
134        self.entries.push(entry);
135        self.append_level(leaf);
136        (self.entries.len() - 1) as u64
137    }
138
139    /// Place `current` into the level tree, walking up to the root.
140    ///
141    /// O(log N): each step derives the value that must sit at the tail
142    /// of the level above — the hash of the last two nodes when the
143    /// level has even count, or a promoted copy of the trailing node
144    /// when odd — and replaces/appends it there. Mirrors exactly what
145    /// [`rebuild_levels`](Self::rebuild_levels) computes.
146    fn append_level(&mut self, leaf: Hash) {
147        if self.levels.is_empty() {
148            // First leaf: single-level tree.
149            self.levels.push(vec![leaf]);
150            self.cached_root = leaf;
151            return;
152        }
153        self.levels[0].push(leaf);
154        let mut j = 0usize;
155        loop {
156            let n = self.levels[j].len();
157            // Value that must be the tail of level j+1.
158            let up = if n % 2 == 0 {
159                let lvl = &self.levels[j];
160                hash_internal(lvl[n - 2], lvl[n - 1])
161            } else {
162                self.levels[j][n - 1]
163            };
164            let target = n.div_ceil(2);
165            j += 1;
166            if j == self.levels.len() {
167                self.levels.push(vec![up]);
168            } else {
169                let above = &mut self.levels[j];
170                if above.len() == target {
171                    // Same slot count — the tail value changed.
172                    *above.last_mut().expect("non-empty level") = up;
173                } else {
174                    // The level below grew odd — its tail needs a new
175                    // promoted slot here.
176                    above.push(up);
177                }
178            }
179            if self.levels[j].len() == 1 {
180                // Root level reached.
181                self.cached_root = self.levels[j][0];
182                return;
183            }
184        }
185    }
186
187    /// Recompute `levels` and `cached_root` from `leaf_hashes`.
188    ///
189    /// Test-only reference implementation: [`append`](Self::append)
190    /// maintains the levels incrementally; this full rebuild exists so
191    /// tests can verify the incremental path produces identical state.
192    #[cfg(test)]
193    fn rebuild_levels(&mut self) {
194        if self.leaf_hashes.is_empty() {
195            self.levels.clear();
196            self.cached_root = [0u8; 32];
197            return;
198        }
199        self.levels.clear();
200        self.levels.push(self.leaf_hashes.clone());
201        while self.levels.last().map_or(0, |l| l.len()) > 1 {
202            let current = self.levels.last().unwrap();
203            let mut next = Vec::with_capacity(current.len() / 2 + 1);
204            let mut iter = current.iter();
205            loop {
206                match (iter.next(), iter.next()) {
207                    (Some(l), Some(r)) => next.push(hash_internal(*l, *r)),
208                    (Some(l), None) => next.push(*l),
209                    _ => break,
210                }
211            }
212            self.levels.push(next);
213        }
214        self.cached_root = self.levels.last().unwrap()[0];
215    }
216
217    /// Current entry count.
218    pub fn len(&self) -> usize {
219        self.entries.len()
220    }
221
222    /// Is the tree empty?
223    pub fn is_empty(&self) -> bool {
224        self.entries.is_empty()
225    }
226
227    /// Compute the current root hash. Empty tree returns all-zeros.
228    ///
229    /// O(1) — the root is incrementally maintained on every `append()`.
230    pub fn root(&self) -> Hash {
231        self.cached_root
232    }
233
234    /// Get an entry by sequence.
235    pub fn entry(&self, sequence: u64) -> Result<&MerkleEntry, MerkleError> {
236        self.entries
237            .get(sequence as usize)
238            .ok_or(MerkleError::OutOfRange(sequence, self.entries.len()))
239    }
240
241    /// Construct an inclusion proof for `sequence`. Returns direction-aware
242    /// proof per RFC 6962 §2.1.1.
243    ///
244    /// O(log N) — walks the cached levels from leaf to root, picking
245    /// the sibling at each level. Compare to the previous O(N)
246    /// implementation which rebuilt every level on each call.
247    pub fn inclusion_proof(&self, sequence: u64) -> Result<InclusionProof, MerkleError> {
248        if sequence as usize >= self.leaf_hashes.len() {
249            return Err(MerkleError::OutOfRange(sequence, self.entries.len()));
250        }
251        // If the levels are empty, every append populates them; a tree
252        // with leaves but no levels cannot be constructed via the
253        // public API. Bail out defensively rather than index out of
254        // bounds if that invariant is ever broken.
255        if self.levels.is_empty() {
256            return Err(MerkleError::OutOfRange(sequence, self.entries.len()));
257        }
258        let mut steps = Vec::new();
259        let mut idx = sequence as usize;
260        for level in 0..self.levels.len().saturating_sub(1) {
261            let current = &self.levels[level];
262            if idx % 2 == 0 {
263                // Current is left child; sibling (if exists) is right
264                let sibling_idx = idx + 1;
265                if sibling_idx < current.len() {
266                    steps.push(ProofStep {
267                        sibling: current[sibling_idx],
268                        side: Side::Right,
269                    });
270                }
271            } else {
272                // Current is right child; sibling is left
273                let sibling_idx = idx - 1;
274                steps.push(ProofStep {
275                    sibling: current[sibling_idx],
276                    side: Side::Left,
277                });
278            }
279            idx /= 2;
280        }
281        Ok(InclusionProof { sequence, steps })
282    }
283
284    /// Verify an inclusion proof (RFC 6962 §2.1.1).
285    pub fn verify_inclusion(
286        entry: &MerkleEntry,
287        proof: &InclusionProof,
288        root: Hash,
289    ) -> Result<(), MerkleError> {
290        let mut current = hash_leaf(entry.entry_hash());
291        for step in &proof.steps {
292            current = match step.side {
293                Side::Left => hash_internal(step.sibling, current),
294                Side::Right => hash_internal(current, step.sibling),
295            };
296        }
297        use subtle::ConstantTimeEq;
298        if current.ct_eq(&root).into() {
299            Ok(())
300        } else {
301            Err(MerkleError::InclusionFailed(entry.sequence))
302        }
303    }
304
305    /// Compute a consistency proof (RFC 6962 §2.1.2).
306    ///
307    /// Proves that the first `old_size` entries of the current tree
308    /// hash to the same root as a tree of exactly `old_size` entries.
309    ///
310    /// Returns the "consistency path" — a list of subtree hashes that
311    /// the verifier uses to reconstruct both the old and new roots.
312    pub fn consistency_proof(&self, old_size: usize) -> Result<Vec<Hash>, MerkleError> {
313        let new_size = self.leaf_hashes.len();
314        if old_size > new_size {
315            return Err(MerkleError::OutOfRange(old_size as u64, new_size));
316        }
317        if old_size == 0 || old_size == new_size {
318            return Ok(Vec::new());
319        }
320        Ok(self.consistency_rec(0, old_size, new_size))
321    }
322
323    /// Recursive helper for [`consistency_proof`](Self::consistency_proof).
324    ///
325    /// Returns the consistency path proving that the first `old_size`
326    /// leaves (starting at offset `start`) hash to the same root as a
327    /// standalone tree of `old_size` leaves, embedded in a larger tree
328    /// of `new_size` leaves.
329    ///
330    /// Algorithm: split `new_size` into a LEFT perfect subtree of size
331    /// `k = largest_pow2_strictly_less_than(new_size)` and a RIGHT
332    /// subtree of size `new_size - k`. Then:
333    ///   - If `old_size <= k`: the old tree is entirely within LEFT.
334    ///     Recurse on LEFT, then append the RIGHT subtree root.
335    ///   - Otherwise: the old tree spans both subtrees. Recurse on
336    ///     RIGHT (with `old_size - k`), then prepend the LEFT subtree
337    ///     root.
338    fn consistency_rec(&self, start: usize, old_size: usize, new_size: usize) -> Vec<Hash> {
339        if old_size == new_size {
340            return Vec::new();
341        }
342        let k = largest_pow2_strictly_less_than(new_size);
343        if old_size <= k {
344            let mut sub = self.consistency_rec(start, old_size, k);
345            sub.push(self.subtree_root(start + k, new_size - k));
346            sub
347        } else {
348            let mut sub = self.consistency_rec(start + k, old_size - k, new_size - k);
349            let mut result = vec![self.subtree_root(start, k)];
350            result.append(&mut sub);
351            result
352        }
353    }
354
355    /// Compute the root of the subtree covering `size` leaves starting
356    /// at offset `start`. Handles arbitrary `size` by decomposing into
357    /// perfect subtrees (compact frontier representation).
358    ///
359    /// For example, `subtree_root(start, 11)` decomposes 11 = 8 + 2 + 1
360    /// and folds the three subtree roots right-to-left per RFC 6962.
361    fn subtree_root(&self, start: usize, size: usize) -> Hash {
362        debug_assert!(
363            start + size <= self.leaf_hashes.len(),
364            "subtree_root: out of range"
365        );
366        if size == 0 {
367            return [0u8; 32];
368        }
369
370        // Decompose `size` into decreasing powers of 2 and compute each
371        // perfect subtree root. Then fold right-to-left: start with the
372        // smallest subtree, combine with each larger one on the left.
373        let mut frontier: Vec<(usize, Hash)> = Vec::new();
374        let mut offset = start;
375        let mut remaining = size;
376        let mut k = 1usize;
377        // Find largest pow2 <= remaining
378        while k * 2 <= remaining {
379            k *= 2;
380        }
381        while remaining > 0 {
382            if remaining >= k {
383                let hash = self.perfect_subtree_root(offset, k);
384                frontier.push((k, hash));
385                offset += k;
386                remaining -= k;
387            }
388            k /= 2;
389        }
390
391        // Fold right-to-left: start with smallest, combine with each
392        // larger one. Result: hash(largest, hash(., hash(smallest)))
393        let mut acc = frontier
394            .last()
395            .expect("non-empty size yields non-empty frontier")
396            .1;
397        for &(_, h) in frontier.iter().rev().skip(1) {
398            acc = hash_internal(h, acc);
399        }
400        acc
401    }
402
403    /// Compute the root of a PERFECT binary subtree of `size` leaves
404    /// starting at `start`. `size` must be a power of 2 and `start` a
405    /// multiple of `size` (the alignment the consistency recursion
406    /// guarantees). Used by [`subtree_root`](Self::subtree_root) for each
407    /// entry in the compact frontier decomposition.
408    ///
409    /// O(1): reads the subtree root straight out of the cached tree
410    /// levels — an aligned perfect subtree of `size = 2^j` leaves rooted
411    /// at leaf offset `start` has its root at
412    /// `levels[j][start / 2^j]`. Falls back to rehashing from the leaves
413    /// when the cache is unavailable (empty tree edge cases).
414    fn perfect_subtree_root(&self, start: usize, size: usize) -> Hash {
415        debug_assert!(
416            size.is_power_of_two(),
417            "perfect_subtree_root: size must be pow2"
418        );
419        debug_assert!(
420            start % size == 0,
421            "perfect_subtree_root: start must be size-aligned"
422        );
423        if size == 1 {
424            return self.leaf_hashes[start];
425        }
426        let j = size.trailing_zeros() as usize; // log2(size)
427        if let Some(level) = self.levels.get(j) {
428            let idx = start / size;
429            if let Some(&h) = level.get(idx) {
430                return h;
431            }
432        }
433        // Cache miss (shouldn't happen for valid ranges) — recompute.
434        let mut level: Vec<Hash> = self.leaf_hashes[start..start + size].to_vec();
435        while level.len() > 1 {
436            let mut next = Vec::with_capacity(level.len() / 2);
437            for chunk in level.chunks(2) {
438                next.push(hash_internal(chunk[0], chunk[1]));
439            }
440            level = next;
441        }
442        level[0]
443    }
444
445    /// Compute the Merkle root of the first `size` leaves. Used by
446    /// [`verify_consistency`](Self::verify_consistency) for brute-force
447    /// verification by recomputing the old tree's root directly.
448    ///
449    /// O(log N): delegates to [`subtree_root`](Self::subtree_root), whose
450    /// frontier decomposition reads each perfect-subtree root from the
451    /// cached levels in O(1).
452    fn root_at_size(&self, size: usize) -> Hash {
453        if size == 0 || size > self.leaf_hashes.len() {
454            return [0u8; 32];
455        }
456        self.subtree_root(0, size)
457    }
458
459    /// Verify a consistency proof (RFC 6962 §2.1.2).
460    ///
461    /// Brute-force verification: recompute the root at `old_size` and
462    /// the current root from this tree's leaves, then compare to
463    /// `old_root` and `new_root` respectively.
464    ///
465    /// This method requires `&self` because external proof-based
466    /// verification (without the tree) requires a much more intricate
467    /// algorithm that handles non-power-of-two `old_size` correctly.
468    /// That algorithm is tracked as a follow-up; for now, this
469    /// brute-force path is correct and is what bindings use.
470    ///
471    /// The `proof` parameter is accepted for forward compatibility —
472    /// it is currently unused (verification is done by direct
473    /// recomputation), but the API shape is preserved so a future
474    /// proof-based verifier can plug in without breaking callers.
475    pub fn verify_consistency(
476        &self,
477        old_root: Hash,
478        new_root: Hash,
479        old_size: usize,
480        new_size: usize,
481        _proof: &[Hash],
482    ) -> Result<(), MerkleError> {
483        if old_size == 0 {
484            return Ok(());
485        }
486        let current_size = self.leaf_hashes.len();
487        if new_size != current_size {
488            return Err(MerkleError::ConsistencyFailed {
489                expected: new_root,
490                actual: self.root(),
491            });
492        }
493        if old_size > current_size {
494            return Err(MerkleError::OutOfRange(old_size as u64, current_size));
495        }
496
497        let computed_old_root = self.root_at_size(old_size);
498        let computed_new_root = self.root();
499
500        use subtle::ConstantTimeEq;
501        let old_ok: bool = computed_old_root.ct_eq(&old_root).into();
502        let new_ok: bool = computed_new_root.ct_eq(&new_root).into();
503        if old_ok && new_ok {
504            Ok(())
505        } else {
506            Err(MerkleError::ConsistencyFailed {
507                expected: old_root,
508                actual: computed_old_root,
509            })
510        }
511    }
512}
513
514#[cfg(test)]
515mod consistency_tests {
516    use super::*;
517    use crate::entry::{ArtifactType, MerkleEntry};
518
519    fn build_tree(n: usize) -> MerkleTree {
520        let mut tree = MerkleTree::new();
521        for i in 0..n {
522            let entry =
523                MerkleEntry::new(i as u64, ArtifactType::CertificateIssuance, [i as u8; 32]);
524            tree.append(entry);
525        }
526        tree
527    }
528
529    #[test]
530    fn consistency_proof_empty_for_same_size() {
531        let tree = build_tree(8);
532        let proof = tree.consistency_proof(8).unwrap();
533        assert!(proof.is_empty());
534    }
535
536    #[test]
537    fn consistency_proof_empty_for_zero() {
538        let tree = build_tree(8);
539        let proof = tree.consistency_proof(0).unwrap();
540        assert!(proof.is_empty());
541    }
542
543    #[test]
544    fn consistency_proof_rejects_old_larger_than_current() {
545        let tree = build_tree(4);
546        assert!(tree.consistency_proof(8).is_err());
547    }
548
549    #[test]
550    fn consistency_proof_returns_subtree_hashes_for_pow2_old_size() {
551        // For (old=4, new=8): proof should contain the right 4-leaf subtree root.
552        let tree = build_tree(8);
553        let proof = tree.consistency_proof(4).unwrap();
554        assert_eq!(proof.len(), 1, "expected single entry for (4, 8)");
555    }
556
557    #[test]
558    fn consistency_proof_returns_multiple_entries_for_non_pow2() {
559        // For (old=3, new=5): generator emits [root_01, lh_3, lh_4].
560        let tree = build_tree(5);
561        let proof = tree.consistency_proof(3).unwrap();
562        assert_eq!(proof.len(), 3, "expected 3 entries for (3, 5)");
563    }
564
565    #[test]
566    fn verify_consistency_accepts_valid_pow2_old_size() {
567        let mut tree = build_tree(8);
568        let old_root = tree.root_at_size(4);
569        // Grow to 12 by appending more entries.
570        for i in 8..12 {
571            let entry =
572                MerkleEntry::new(i as u64, ArtifactType::CertificateIssuance, [i as u8; 32]);
573            tree.append(entry);
574        }
575        let new_root = tree.root();
576        let proof = tree.consistency_proof(4).unwrap();
577        tree.verify_consistency(old_root, new_root, 4, 12, &proof)
578            .expect("must verify for valid pow2 old_size");
579    }
580
581    #[test]
582    fn verify_consistency_accepts_valid_non_pow2_old_size() {
583        let mut tree = build_tree(5);
584        let old_root = tree.root_at_size(3);
585        for i in 5..11 {
586            let entry =
587                MerkleEntry::new(i as u64, ArtifactType::CertificateIssuance, [i as u8; 32]);
588            tree.append(entry);
589        }
590        let new_root = tree.root();
591        let proof = tree.consistency_proof(3).unwrap();
592        tree.verify_consistency(old_root, new_root, 3, 11, &proof)
593            .expect("must verify for valid non-pow2 old_size");
594    }
595
596    #[test]
597    fn verify_consistency_detects_tampered_old_root() {
598        let mut tree = build_tree(8);
599        for i in 8..12 {
600            let entry =
601                MerkleEntry::new(i as u64, ArtifactType::CertificateIssuance, [i as u8; 32]);
602            tree.append(entry);
603        }
604        let new_root = tree.root();
605        let proof = tree.consistency_proof(4).unwrap();
606        let bogus_old_root = [0xffu8; 32];
607        let result = tree.verify_consistency(bogus_old_root, new_root, 4, 12, &proof);
608        assert!(matches!(result, Err(MerkleError::ConsistencyFailed { .. })));
609    }
610
611    #[test]
612    fn verify_consistency_detects_tampered_new_root() {
613        let mut tree = build_tree(8);
614        let old_root = tree.root_at_size(4);
615        for i in 8..12 {
616            let entry =
617                MerkleEntry::new(i as u64, ArtifactType::CertificateIssuance, [i as u8; 32]);
618            tree.append(entry);
619        }
620        let proof = tree.consistency_proof(4).unwrap();
621        let bogus_new_root = [0xffu8; 32];
622        let result = tree.verify_consistency(old_root, bogus_new_root, 4, 12, &proof);
623        assert!(matches!(result, Err(MerkleError::ConsistencyFailed { .. })));
624    }
625
626    #[test]
627    fn verify_consistency_accepts_all_sizes_1_to_16() {
628        // Comprehensive: grow the tree from 1 to 16 leaves. At each
629        // step, every prior size is a valid old_size. Verify them all.
630        let mut tree = MerkleTree::new();
631        let mut roots: Vec<Hash> = Vec::new();
632        for i in 0..16u64 {
633            let entry = MerkleEntry::new(i, ArtifactType::CertificateIssuance, [i as u8; 32]);
634            tree.append(entry);
635            roots.push(tree.root());
636        }
637        let final_size = tree.leaf_hashes.len();
638        for old_size in 1..=final_size {
639            let old_root = roots[old_size - 1];
640            let new_root = roots[final_size - 1];
641            let proof = tree.consistency_proof(old_size).unwrap();
642            tree.verify_consistency(old_root, new_root, old_size, final_size, &proof)
643                .unwrap_or_else(|e| {
644                    panic!("verify_consistency failed for old_size={old_size}: {e:?}")
645                });
646        }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653    use crate::entry::ArtifactType;
654
655    #[test]
656    fn incremental_levels_match_rebuild_at_every_size() {
657        // The O(log N) incremental append must produce byte-identical
658        // levels + root to the full-rebuild reference, at every size.
659        let mut tree = MerkleTree::new();
660        for i in 0..64u64 {
661            tree.append(MerkleEntry::new(
662                i,
663                ArtifactType::CertificateIssuance,
664                [(i as u8).wrapping_mul(7); 32],
665            ));
666            let incremental_levels = tree.levels.clone();
667            let incremental_root = tree.cached_root;
668            tree.rebuild_levels();
669            assert_eq!(
670                incremental_levels, tree.levels,
671                "levels diverge after append {i}"
672            );
673            assert_eq!(incremental_root, tree.cached_root, "root diverges at {i}");
674        }
675    }
676
677    #[test]
678    fn empty_tree_has_zero_root() {
679        let tree = MerkleTree::new();
680        assert_eq!(tree.root(), [0u8; 32]);
681    }
682
683    #[test]
684    fn single_entry_tree() {
685        let mut tree = MerkleTree::new();
686        let entry = MerkleEntry::new(0, ArtifactType::CertificateIssuance, [1u8; 32]);
687        tree.append(entry);
688        let root = tree.root();
689        assert_ne!(root, [0u8; 32]);
690    }
691
692    #[test]
693    fn multiple_entries_produce_different_root() {
694        let mut tree1 = MerkleTree::new();
695        let mut tree2 = MerkleTree::new();
696
697        tree1.append(MerkleEntry::new(
698            0,
699            ArtifactType::CertificateIssuance,
700            [1u8; 32],
701        ));
702        tree1.append(MerkleEntry::new(
703            1,
704            ArtifactType::CertificateIssuance,
705            [2u8; 32],
706        ));
707
708        tree2.append(MerkleEntry::new(
709            0,
710            ArtifactType::CertificateIssuance,
711            [1u8; 32],
712        ));
713        tree2.append(MerkleEntry::new(
714            1,
715            ArtifactType::CertificateIssuance,
716            [3u8; 32],
717        ));
718
719        assert_ne!(tree1.root(), tree2.root());
720    }
721
722    #[test]
723    fn inclusion_proof_round_trip() {
724        let mut tree = MerkleTree::new();
725        let mut entries = Vec::new();
726        for i in 0..5u64 {
727            let e = MerkleEntry::new(i, ArtifactType::CertificateIssuance, [i as u8; 32]);
728            entries.push(e.clone());
729            tree.append(e);
730        }
731        let root = tree.root();
732        // Verify every leaf has a valid inclusion proof
733        for i in 0..5 {
734            let proof = tree.inclusion_proof(i).unwrap();
735            MerkleTree::verify_inclusion(&entries[i as usize], &proof, root)
736                .expect("inclusion proof must verify");
737        }
738    }
739
740    #[test]
741    fn inclusion_proof_negative_case() {
742        let mut tree = MerkleTree::new();
743        let entries: Vec<MerkleEntry> = (0..5u64)
744            .map(|i| MerkleEntry::new(i, ArtifactType::CertificateIssuance, [i as u8; 32]))
745            .collect();
746        for e in &entries {
747            tree.append(e.clone());
748        }
749        let root = tree.root();
750        // Use proof for entry 2 but try to verify entry 3
751        let wrong_proof = tree.inclusion_proof(2).unwrap();
752        let result = MerkleTree::verify_inclusion(&entries[3], &wrong_proof, root);
753        assert!(matches!(result, Err(MerkleError::InclusionFailed(_))));
754    }
755
756    #[test]
757    fn inclusion_proof_power_of_two_tree() {
758        // 8 entries (power of 2) — clean binary tree
759        let mut tree = MerkleTree::new();
760        let entries: Vec<MerkleEntry> = (0..8u64)
761            .map(|i| MerkleEntry::new(i, ArtifactType::CertificateIssuance, [i as u8; 32]))
762            .collect();
763        for e in &entries {
764            tree.append(e.clone());
765        }
766        let root = tree.root();
767        for i in 0..8 {
768            let proof = tree.inclusion_proof(i).unwrap();
769            MerkleTree::verify_inclusion(&entries[i as usize], &proof, root).expect("must verify");
770        }
771    }
772
773    #[test]
774    fn out_of_range_returns_error() {
775        let tree = MerkleTree::new();
776        let result = tree.entry(0);
777        assert!(matches!(result, Err(MerkleError::OutOfRange(_, _))));
778    }
779}
780
781#[cfg(test)]
782mod proptests {
783    use super::*;
784    use crate::entry::ArtifactType;
785    use proptest::prelude::*;
786
787    fn arb_entry(seq: u64) -> MerkleEntry {
788        let mut hash = [0u8; 32];
789        // Mix seq into a few bytes so each entry has a distinct hash.
790        hash[0..8].copy_from_slice(&seq.to_le_bytes());
791        MerkleEntry::new(seq, ArtifactType::CertificateIssuance, hash)
792    }
793
794    // For any tree size N in [1, 100], every leaf's inclusion proof
795    // verifies against the current root.
796    proptest! {
797        #[test]
798        fn every_leaf_inclusion_proof_verifies(n in 1u64..100) {
799            let mut tree = MerkleTree::new();
800            let entries: Vec<MerkleEntry> = (0..n).map(arb_entry).collect();
801            for e in &entries {
802                tree.append(e.clone());
803            }
804            let root = tree.root();
805            for seq in 0..n {
806                let proof = tree.inclusion_proof(seq)?;
807                MerkleTree::verify_inclusion(&entries[seq as usize], &proof, root)?;
808            }
809        }
810    }
811
812    // A proof for one entry must NOT verify a different entry.
813    proptest! {
814        #[test]
815        fn inclusion_proof_rejects_wrong_entry(n in 2u64..50, i in 0u64..50, j in 0u64..50) {
816            prop_assume!(n > 1 && i < n && j < n && i != j);
817            let mut tree = MerkleTree::new();
818            let entries: Vec<MerkleEntry> = (0..n).map(arb_entry).collect();
819            for e in &entries {
820                tree.append(e.clone());
821            }
822            let root = tree.root();
823            let proof = tree.inclusion_proof(i)?;
824            let result = MerkleTree::verify_inclusion(&entries[j as usize], &proof, root);
825            prop_assert!(matches!(result, Err(MerkleError::InclusionFailed(_))));
826        }
827    }
828
829    // Appending an entry changes the root (log is append-only and
830    // every append is reflected in the commitment).
831    proptest! {
832        #[test]
833        fn append_changes_root(n in 0u64..50) {
834            let mut tree = MerkleTree::new();
835            for seq in 0..n {
836                tree.append(arb_entry(seq));
837            }
838            let root_before = tree.root();
839            tree.append(arb_entry(n));
840            let root_after = tree.root();
841            prop_assert_ne!(root_before, root_after);
842        }
843    }
844
845    // Empty tree's root is the all-zero hash (RFC 6962 convention).
846    #[test]
847    fn empty_tree_root_is_zero() {
848        let tree = MerkleTree::new();
849        assert_eq!(tree.root(), [0u8; 32]);
850    }
851}