Skip to main content

treeship_core/merkle/
tree.rs

1use sha2::{Sha256, Digest};
2use serde::{Deserialize, Serialize};
3
4/// Errors raised by Merkle primitives and tree construction.
5///
6/// Currently only one variant: an unknown `merkle_version` byte. Kept as
7/// an enum so future version-specific validation can be added without
8/// breaking the public signature.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum MerkleError {
11    /// The supplied `merkle_version` is neither v1 nor v2 (the only
12    /// recognised versions). Surfaces at `MerkleTree::with_version`,
13    /// at every callsite that constructs a tree from a receipt, and
14    /// at proof verification.
15    UnknownVersion(u8),
16}
17
18impl std::fmt::Display for MerkleError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            Self::UnknownVersion(v) => write!(
22                f,
23                "unknown merkle_version {} (expected {} or {})",
24                v, MERKLE_VERSION_V1, MERKLE_VERSION_V2,
25            ),
26        }
27    }
28}
29
30impl std::error::Error for MerkleError {}
31
32/// Direction of a sibling in the Merkle proof path.
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub enum Direction {
35    Left,
36    Right,
37}
38
39/// One step in an inclusion proof path.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ProofStep {
42    pub direction: Direction,
43    /// Hex-encoded hash of the sibling node.
44    pub hash: String,
45}
46
47/// Merkle tree algorithm identifier for forward/backward compatibility.
48pub const MERKLE_ALGORITHM_V1: &str = "sha256-duplicate-last";
49pub const MERKLE_ALGORITHM_V2: &str = "sha256-rfc9162";
50
51/// Merkle format version byte. Distinguishes pre-domain-separation hashing
52/// (v1, used through v0.10.2) from RFC 9162 domain-separated hashing (v2,
53/// default from v0.10.3 onward).
54///
55/// - `1` — legacy: `sha256(artifact_id)` for leaves, `sha256(L || R)` for
56///   internal nodes. No domain separation. Vulnerable to second-preimage
57///   confusion between leaf and internal nodes when the artifact ID happens
58///   to be 32 or 64 bytes. Retained ONLY so v0.10.2-and-earlier receipts
59///   continue to verify during the deprecation window.
60/// - `2` — RFC 9162: `sha256(0x00 || artifact_id_bytes)` for leaves,
61///   `sha256(0x01 || L || R)` for internal nodes. Leaf and internal
62///   pre-images cannot collide.
63///
64/// v1 verification is scheduled for removal in v0.13.0.
65pub const MERKLE_VERSION_V1: u8 = 1;
66pub const MERKLE_VERSION_V2: u8 = 2;
67
68/// Default merkle version used by `#[serde(default = ...)]` so receipts
69/// produced before the field existed deserialize as v1 (the pre-domain-
70/// separation hashing that was in effect at the time).
71pub fn default_merkle_version_v1() -> u8 {
72    MERKLE_VERSION_V1
73}
74
75// ── Domain-separated hash primitives ─────────────────────────────────
76//
77// These are crate-internal so the version dispatch lives in one place.
78// Tests can reach them through `#[cfg(test)]` re-exports if they need
79// to pin a byte-identical output.
80
81/// v1 (legacy) leaf hash: `sha256(artifact_id_bytes)`. No domain prefix.
82pub(crate) fn hash_leaf_v1(artifact_id: &str) -> [u8; 32] {
83    Sha256::digest(artifact_id.as_bytes()).into()
84}
85
86/// v1 (legacy) internal node hash: `sha256(left || right)`. No domain prefix.
87pub(crate) fn hash_internal_v1(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
88    let mut h = Sha256::new();
89    h.update(left);
90    h.update(right);
91    h.finalize().into()
92}
93
94/// v2 leaf hash per RFC 9162: `sha256(0x00 || artifact_id_bytes)`.
95pub(crate) fn hash_leaf_v2(artifact_id: &str) -> [u8; 32] {
96    let mut h = Sha256::new();
97    h.update([0x00u8]);
98    h.update(artifact_id.as_bytes());
99    h.finalize().into()
100}
101
102/// v2 internal node hash per RFC 9162: `sha256(0x01 || left || right)`.
103pub(crate) fn hash_internal_v2(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
104    let mut h = Sha256::new();
105    h.update([0x01u8]);
106    h.update(left);
107    h.update(right);
108    h.finalize().into()
109}
110
111/// Version-dispatched leaf hash. Rejects unknown versions instead of
112/// falling back: a silent fallback to v1 was the original downgrade
113/// vector — a receipt could claim `merkle_version: 99` and get
114/// recomputed under v1 hashing.
115pub(crate) fn hash_leaf(version: u8, artifact_id: &str) -> Result<[u8; 32], MerkleError> {
116    match version {
117        MERKLE_VERSION_V1 => Ok(hash_leaf_v1(artifact_id)),
118        MERKLE_VERSION_V2 => Ok(hash_leaf_v2(artifact_id)),
119        other => Err(MerkleError::UnknownVersion(other)),
120    }
121}
122
123/// Version-dispatched internal node hash. Rejects unknown versions
124/// for the same reason as [`hash_leaf`].
125pub(crate) fn hash_internal(
126    version: u8,
127    left: &[u8; 32],
128    right: &[u8; 32],
129) -> Result<[u8; 32], MerkleError> {
130    match version {
131        MERKLE_VERSION_V1 => Ok(hash_internal_v1(left, right)),
132        MERKLE_VERSION_V2 => Ok(hash_internal_v2(left, right)),
133        other => Err(MerkleError::UnknownVersion(other)),
134    }
135}
136
137/// An inclusion proof: the sibling path from a leaf to the root.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct InclusionProof {
140    pub leaf_index: usize,
141    /// Hex-encoded leaf hash.
142    pub leaf_hash: String,
143    pub path: Vec<ProofStep>,
144    /// Algorithm used to build this proof. Missing = v1 (duplicate-last).
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub algorithm: Option<String>,
147    /// Merkle format version byte. Drives the leaf and internal hash
148    /// dispatch at verify time. Missing on pre-v0.10.3 proofs — defaults
149    /// to `1` (no domain separation) so v0.10.2 receipts continue to
150    /// verify. New proofs always serialize `2`.
151    #[serde(default = "default_merkle_version_v1")]
152    pub merkle_version: u8,
153}
154
155/// An append-only binary Merkle tree.
156///
157/// Domain separation per RFC 9162 (when `version == 2`): leaves are
158/// `sha256(0x00 || artifact_id)` and internal nodes are
159/// `sha256(0x01 || left || right)`. Pre-domain-separation hashing
160/// (`version == 1`) is retained ONLY for the deprecation window so
161/// v0.10.2-and-earlier receipts continue to round-trip; new trees
162/// default to v2.
163///
164/// Odd leaf counts are handled by promoting the unpaired node to the
165/// next level without hashing, matching RFC 9162 (Certificate
166/// Transparency) construction.
167pub struct MerkleTree {
168    /// All leaf hashes in insertion order.
169    leaves: Vec<[u8; 32]>,
170    /// Hash version used for leaf + internal-node hashing.
171    version: u8,
172}
173
174impl Default for MerkleTree {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl MerkleTree {
181    /// New tree using the current default version (v2, domain-separated).
182    pub fn new() -> Self {
183        // SAFETY: V2 is a recognised version, so with_version cannot fail.
184        Self::with_version(MERKLE_VERSION_V2)
185            .expect("MERKLE_VERSION_V2 is always a valid version")
186    }
187
188    /// New tree pinned to an explicit hash version. Returns
189    /// `Err(MerkleError::UnknownVersion)` if `version` is anything other
190    /// than [`MERKLE_VERSION_V1`] or [`MERKLE_VERSION_V2`]. Validating
191    /// at construction time is what lets `append`, `root`, and
192    /// `inclusion_proof` keep their infallible signatures.
193    pub fn with_version(version: u8) -> Result<Self, MerkleError> {
194        if version != MERKLE_VERSION_V1 && version != MERKLE_VERSION_V2 {
195            return Err(MerkleError::UnknownVersion(version));
196        }
197        Ok(Self { leaves: Vec::new(), version })
198    }
199
200    /// Hash version this tree builds proofs under.
201    pub fn version(&self) -> u8 {
202        self.version
203    }
204
205    /// Append an artifact ID as a new leaf. Returns the leaf index.
206    pub fn append(&mut self, artifact_id: &str) -> usize {
207        // Invariant: `self.version` was validated by `with_version`, so
208        // hash_leaf cannot return UnknownVersion here.
209        let hash = hash_leaf(self.version, artifact_id)
210            .expect("tree version validated at construction");
211        self.leaves.push(hash);
212        self.leaves.len() - 1
213    }
214
215    /// Number of leaves in the tree.
216    pub fn len(&self) -> usize {
217        self.leaves.len()
218    }
219
220    /// Whether the tree is empty.
221    pub fn is_empty(&self) -> bool {
222        self.leaves.is_empty()
223    }
224
225    /// Compute the root hash. Returns `None` for an empty tree.
226    pub fn root(&self) -> Option<[u8; 32]> {
227        if self.leaves.is_empty() {
228            return None;
229        }
230        Some(self.compute_root(&self.leaves))
231    }
232
233    /// Height of the tree: ceil(log2(n_leaves)).
234    pub fn height(&self) -> usize {
235        if self.leaves.len() <= 1 {
236            return 0;
237        }
238        (self.leaves.len() as f64).log2().ceil() as usize
239    }
240
241    /// Generate an inclusion proof for the leaf at `leaf_index`.
242    pub fn inclusion_proof(&self, leaf_index: usize) -> Option<InclusionProof> {
243        if leaf_index >= self.leaves.len() {
244            return None;
245        }
246
247        let mut path = Vec::new();
248        let mut idx = leaf_index;
249        let mut level: Vec<[u8; 32]> = self.leaves.clone();
250
251        while level.len() > 1 {
252            // RFC 9162: if idx has a sibling, add it to the proof path.
253            // If idx is the unpaired last node, it promotes without a sibling step.
254            if idx + 1 < level.len() && idx % 2 == 0 {
255                // Sibling is to the right
256                path.push(ProofStep {
257                    direction: Direction::Right,
258                    hash: hex::encode(level[idx + 1]),
259                });
260            } else if idx % 2 == 1 {
261                // Sibling is to the left
262                path.push(ProofStep {
263                    direction: Direction::Left,
264                    hash: hex::encode(level[idx - 1]),
265                });
266            }
267            // else: unpaired last node, no sibling step needed
268
269            // Move up: compute parent hashes (RFC 9162 promotion). The
270            // tree-version invariant guarantees hash_internal cannot
271            // return UnknownVersion.
272            let mut next_level = Vec::with_capacity((level.len() + 1) / 2);
273            let mut i = 0;
274            while i + 1 < level.len() {
275                next_level.push(
276                    hash_internal(self.version, &level[i], &level[i + 1])
277                        .expect("tree version validated at construction"),
278                );
279                i += 2;
280            }
281            if i < level.len() {
282                next_level.push(level[i]);
283            }
284            level = next_level;
285
286            idx /= 2;
287        }
288
289        Some(InclusionProof {
290            leaf_index,
291            leaf_hash: hex::encode(self.leaves[leaf_index]),
292            path,
293            algorithm: Some(match self.version {
294                MERKLE_VERSION_V1 => MERKLE_ALGORITHM_V1.to_string(),
295                _ => MERKLE_ALGORITHM_V2.to_string(),
296            }),
297            merkle_version: self.version,
298        })
299    }
300
301    /// Verify an inclusion proof against a hex-encoded root hash.
302    ///
303    /// Recomputes the root from `artifact_id` + the proof path and checks
304    /// that it matches `root_hex`. Fully offline, no tree state needed.
305    ///
306    /// `expected_version` is the **trusted** merkle version — typically the
307    /// merkle_version pulled from a signature-verified checkpoint or from
308    /// the parent receipt's merkle section. The verifier hashes under
309    /// `expected_version` and *additionally* rejects the proof if
310    /// `proof.merkle_version != expected_version`. This is what closes
311    /// the downgrade vector: the version that drives hashing is taken
312    /// from a trusted source, not from the (attacker-controlled) proof
313    /// blob.
314    ///
315    /// Rejects:
316    /// - `expected_version` outside {1, 2}
317    /// - `proof.merkle_version != expected_version`
318    /// - in v2, an empty path with `leaf_index != 0` — closes the
319    ///   internal-node-as-leaf impersonation that v1 permitted.
320    pub fn verify_proof(
321        expected_version: u8,
322        root_hex: &str,
323        artifact_id: &str,
324        proof: &InclusionProof,
325    ) -> bool {
326        // The trusted version itself must be a known one. Unknown =
327        // misconfiguration upstream; reject loudly.
328        if expected_version != MERKLE_VERSION_V1 && expected_version != MERKLE_VERSION_V2 {
329            return false;
330        }
331
332        // The proof's self-declared version must agree with the trusted
333        // version. Mismatch = downgrade attempt (or honest drift —
334        // either way, refuse).
335        if proof.merkle_version != expected_version {
336            return false;
337        }
338
339        // Validate algorithm string ONLY when present. v0.10.2 receipts
340        // carry `algorithm = "sha256-rfc9162"` even though no domain
341        // separation was applied — we no longer let the string drive
342        // dispatch; merkle_version does. Unknown strings still reject.
343        if let Some(algo) = proof.algorithm.as_deref() {
344            if algo != MERKLE_ALGORITHM_V1 && algo != MERKLE_ALGORITHM_V2 {
345                return false;
346            }
347        }
348
349        // v2 hardens the single-leaf path. An empty proof path is only
350        // legitimate when this leaf IS the root (single-leaf tree at
351        // index 0). In v1 we cannot enforce this without breaking
352        // legacy receipts, so the check is v2-only.
353        if expected_version == MERKLE_VERSION_V2 && proof.path.is_empty() && proof.leaf_index != 0 {
354            return false;
355        }
356
357        let current: [u8; 32] = match hash_leaf(expected_version, artifact_id) {
358            Ok(h) => h,
359            Err(_) => return false,
360        };
361        // Verify leaf hash matches artifact
362        if hex::encode(current) != proof.leaf_hash {
363            return false;
364        }
365
366        let mut current = current;
367        for step in &proof.path {
368            let sibling = match hex::decode(&step.hash) {
369                Ok(b) if b.len() == 32 => {
370                    let mut arr = [0u8; 32];
371                    arr.copy_from_slice(&b);
372                    arr
373                }
374                _ => return false,
375            };
376
377            current = match step.direction {
378                Direction::Right => match hash_internal(expected_version, &current, &sibling) {
379                    Ok(h) => h,
380                    Err(_) => return false,
381                },
382                Direction::Left => match hash_internal(expected_version, &sibling, &current) {
383                    Ok(h) => h,
384                    Err(_) => return false,
385                },
386            };
387        }
388
389        hex::encode(current) == root_hex
390    }
391
392    /// Internal: compute root from a slice of leaf hashes.
393    /// RFC 9162 construction: odd nodes are promoted without hashing.
394    fn compute_root(&self, leaves: &[[u8; 32]]) -> [u8; 32] {
395        if leaves.len() == 1 {
396            return leaves[0];
397        }
398        let mut level = leaves.to_vec();
399        while level.len() > 1 {
400            let mut next_level = Vec::with_capacity((level.len() + 1) / 2);
401            let mut i = 0;
402            while i + 1 < level.len() {
403                // Invariant: tree version validated at construction.
404                next_level.push(
405                    hash_internal(self.version, &level[i], &level[i + 1])
406                        .expect("tree version validated at construction"),
407                );
408                i += 2;
409            }
410            // RFC 9162: promote unpaired node without hashing
411            if i < level.len() {
412                next_level.push(level[i]);
413            }
414            level = next_level;
415        }
416        level[0]
417    }
418
419    /// Generate a consistency proof from this tree (current size `n`) back to an
420    /// earlier `old_size` `m` (`0 < m <= n`): a minimal set of subtree hashes
421    /// that lets a verifier reconstruct **both** the size-`m` root and the
422    /// size-`n` root, proving the first `m` leaves were not rewritten (the
423    /// log only appended). Returns `None` for `old_size == 0` or `> n`; an
424    /// empty proof for `old_size == n`. See docs/specs/merkle-consistency.md.
425    pub fn consistency_proof(&self, old_size: usize) -> Option<Vec<String>> {
426        let n = self.leaves.len();
427        if old_size == 0 || old_size > n {
428            return None;
429        }
430        if old_size == n {
431            return Some(Vec::new());
432        }
433        Some(
434            subproof(self.version, &self.leaves[..n], old_size, true)
435                .into_iter()
436                .map(hex::encode)
437                .collect(),
438        )
439    }
440}
441
442/// Merkle Tree Hash of a slice of leaf hashes, the same level-by-level
443/// promotion `compute_root` uses, free-standing and version-parameterized.
444fn merkle_root(version: u8, leaves: &[[u8; 32]]) -> [u8; 32] {
445    if leaves.len() == 1 {
446        return leaves[0];
447    }
448    let mut level = leaves.to_vec();
449    while level.len() > 1 {
450        let mut next = Vec::with_capacity(level.len().div_ceil(2));
451        let mut i = 0;
452        while i + 1 < level.len() {
453            next.push(
454                hash_internal(version, &level[i], &level[i + 1])
455                    .expect("tree version validated at construction"),
456            );
457            i += 2;
458        }
459        if i < level.len() {
460            next.push(level[i]);
461        }
462        level = next;
463    }
464    level[0]
465}
466
467/// Largest power of two strictly less than `n` (requires `n >= 2`).
468fn largest_pow2_lt(n: usize) -> usize {
469    let mut k = 1usize;
470    while k < n {
471        k <<= 1;
472    }
473    k >> 1
474}
475
476/// RFC 6962 SUBPROOF over the first `n` leaves, committing to the first `m`.
477/// `b` tracks whether the `m`-subtree is the one being proved as a prefix.
478fn subproof(version: u8, leaves: &[[u8; 32]], m: usize, b: bool) -> Vec<[u8; 32]> {
479    let n = leaves.len();
480    if m == n {
481        return if b {
482            Vec::new()
483        } else {
484            vec![merkle_root(version, leaves)]
485        };
486    }
487    let k = largest_pow2_lt(n);
488    if m <= k {
489        let mut p = subproof(version, &leaves[..k], m, b);
490        p.push(merkle_root(version, &leaves[k..]));
491        p
492    } else {
493        let mut p = subproof(version, &leaves[k..], m - k, false);
494        p.push(merkle_root(version, &leaves[..k]));
495        p
496    }
497}
498
499fn decode_hash(hex_str: &str) -> Option<[u8; 32]> {
500    let bytes = hex::decode(hex_str).ok()?;
501    if bytes.len() != 32 {
502        return None;
503    }
504    let mut arr = [0u8; 32];
505    arr.copy_from_slice(&bytes);
506    Some(arr)
507}
508
509/// Verify, offline, that the tree of `new_size`/`new_root_hex` **extends** the
510/// tree of `old_size`/`old_root_hex`: the proof reconstructs both roots and
511/// they match. RFC 6962 consistency verification, hashing under the trusted
512/// `version`. Rejects on any mismatch, bad hex, or leftover proof elements.
513pub fn verify_consistency(
514    version: u8,
515    old_size: usize,
516    old_root_hex: &str,
517    new_size: usize,
518    new_root_hex: &str,
519    proof: &[String],
520) -> bool {
521    if version != MERKLE_VERSION_V1 && version != MERKLE_VERSION_V2 {
522        return false;
523    }
524    let (Some(old_root), Some(new_root)) =
525        (decode_hash(old_root_hex), decode_hash(new_root_hex))
526    else {
527        return false;
528    };
529    if old_size > new_size {
530        return false;
531    }
532    if old_size == new_size {
533        return proof.is_empty() && old_root == new_root;
534    }
535    if old_size == 0 {
536        return proof.is_empty();
537    }
538
539    let mut it = proof.iter();
540    let mut node = old_size - 1;
541    let mut last = new_size - 1;
542    // Walk up to the first node that is a left child / a fork between m and n.
543    while node & 1 == 1 {
544        node >>= 1;
545        last >>= 1;
546    }
547    // Seed: if old_size is a power of two (node == 0) the old root seeds both;
548    // otherwise the first proof element does.
549    let (mut old_hash, mut new_hash) = if node != 0 {
550        let Some(seed) = it.next().and_then(|s| decode_hash(s)) else {
551            return false;
552        };
553        (seed, seed)
554    } else {
555        (old_root, old_root)
556    };
557    while node != 0 {
558        if node & 1 == 1 {
559            let Some(sib) = it.next().and_then(|s| decode_hash(s)) else {
560                return false;
561            };
562            old_hash = hash_internal(version, &sib, &old_hash).expect("version validated");
563            new_hash = hash_internal(version, &sib, &new_hash).expect("version validated");
564        } else if node < last {
565            let Some(sib) = it.next().and_then(|s| decode_hash(s)) else {
566                return false;
567            };
568            new_hash = hash_internal(version, &new_hash, &sib).expect("version validated");
569        }
570        node >>= 1;
571        last >>= 1;
572    }
573    // Fold the remaining right-edge siblings into the new root.
574    while last != 0 {
575        let Some(sib) = it.next().and_then(|s| decode_hash(s)) else {
576            return false;
577        };
578        new_hash = hash_internal(version, &new_hash, &sib).expect("version validated");
579        last >>= 1;
580    }
581    it.next().is_none() && old_hash == old_root && new_hash == new_root
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    /// The root of a tree built from the first `m` ids, hex-encoded.
589    fn prefix_root(version: u8, ids: &[String], m: usize) -> String {
590        let mut t = MerkleTree::with_version(version).unwrap();
591        for id in &ids[..m] {
592            t.append(id);
593        }
594        hex::encode(t.root().unwrap())
595    }
596
597    /// The validating test: for every (m, n), the consistency proof from m must
598    /// reconstruct BOTH real roots (compute_root is ground truth for this tree
599    /// shape). A wrong algorithm fails here loudly. Tampering must be rejected.
600    #[test]
601    fn consistency_roundtrip_and_tamper() {
602        // 1..=20 covers every structural case: power-of-two boundaries
603        // (2/4/8/16), the post-boundary sizes (17..20), and all promotion
604        // patterns. Kept bounded so the O(n^2) prefix rebuilds stay CI-fast.
605        for &version in &[MERKLE_VERSION_V1, MERKLE_VERSION_V2] {
606            for n in 1..=20usize {
607                let ids: Vec<String> = (0..n).map(|i| format!("art_{i:06x}")).collect();
608                let mut tree = MerkleTree::with_version(version).unwrap();
609                for id in &ids {
610                    tree.append(id);
611                }
612                let root_n = hex::encode(tree.root().unwrap());
613
614                for m in 1..=n {
615                    let root_m = prefix_root(version, &ids, m);
616                    let proof = tree.consistency_proof(m).expect("proof exists");
617
618                    assert!(
619                        verify_consistency(version, m, &root_m, n, &root_n, &proof),
620                        "v{version}: consistency m={m} n={n} should verify"
621                    );
622
623                    if m < n {
624                        let zero = "00".repeat(32);
625                        // Wrong old root, wrong new root -> reject.
626                        assert!(
627                            !verify_consistency(version, m, &zero, n, &root_n, &proof),
628                            "v{version} m={m} n={n}: tampered old_root must reject"
629                        );
630                        assert!(
631                            !verify_consistency(version, m, &root_m, n, &zero, &proof),
632                            "v{version} m={m} n={n}: tampered new_root must reject"
633                        );
634                        // Mutated proof element (if any) -> reject.
635                        if !proof.is_empty() {
636                            let mut bad = proof.clone();
637                            bad[0] = zero.clone();
638                            assert!(
639                                !verify_consistency(version, m, &root_m, n, &root_n, &bad),
640                                "v{version} m={m} n={n}: mutated proof must reject"
641                            );
642                        }
643                    }
644                }
645            }
646        }
647    }
648
649    #[test]
650    fn consistency_edge_cases() {
651        let mut t = MerkleTree::default();
652        for i in 0..5 {
653            t.append(&format!("art_{i}"));
654        }
655        let root5 = hex::encode(t.root().unwrap());
656        // old_size == new_size: empty proof, equal roots.
657        assert_eq!(t.consistency_proof(5), Some(Vec::new()));
658        assert!(verify_consistency(t.version(), 5, &root5, 5, &root5, &[]));
659        // out of range.
660        assert_eq!(t.consistency_proof(0), None);
661        assert_eq!(t.consistency_proof(6), None);
662        // old_size > new_size rejected at verify time.
663        assert!(!verify_consistency(t.version(), 6, &root5, 5, &root5, &[]));
664        // unknown version rejected.
665        assert!(!verify_consistency(99, 1, &root5, 5, &root5, &[]));
666    }
667
668    #[test]
669    fn single_leaf_root_is_leaf_hash() {
670        // Tree built with the current default (v2). The root of a
671        // single-leaf tree equals the v2 leaf hash of the artifact.
672        let mut tree = MerkleTree::new();
673        tree.append("art_abc123");
674        let root = tree.root().unwrap();
675        let expected = hash_leaf_v2("art_abc123");
676        assert_eq!(root, expected);
677    }
678
679    #[test]
680    fn single_leaf_root_is_leaf_hash_v1_legacy() {
681        let mut tree = MerkleTree::with_version(MERKLE_VERSION_V1).unwrap();
682        tree.append("art_abc123");
683        let root = tree.root().unwrap();
684        let expected = hash_leaf_v1("art_abc123");
685        assert_eq!(root, expected);
686    }
687
688    #[test]
689    fn inclusion_proof_verifies() {
690        let mut tree = MerkleTree::new();
691        let ids = ["art_a", "art_b", "art_c", "art_d"];
692        for id in &ids {
693            tree.append(id);
694        }
695
696        let root = hex::encode(tree.root().unwrap());
697        let proof = tree.inclusion_proof(1).unwrap(); // art_b at index 1
698
699        assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_b", &proof));
700    }
701
702    #[test]
703    fn wrong_artifact_fails_verification() {
704        let mut tree = MerkleTree::new();
705        tree.append("art_a");
706        tree.append("art_b");
707
708        let root = hex::encode(tree.root().unwrap());
709        let proof = tree.inclusion_proof(0).unwrap(); // proof for art_a
710
711        // Try to verify art_WRONG against art_a's proof
712        assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_WRONG", &proof));
713    }
714
715    #[test]
716    fn tampered_sibling_fails() {
717        let mut tree = MerkleTree::new();
718        tree.append("art_a");
719        tree.append("art_b");
720
721        let root = hex::encode(tree.root().unwrap());
722        let mut proof = tree.inclusion_proof(0).unwrap();
723
724        // Tamper with a sibling hash
725        proof.path[0].hash = "0".repeat(64);
726
727        assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &proof));
728    }
729
730    // ── v2 hash primitives ─────────────────────────────────────────
731    // These tests pin the byte-identical output of the new
732    // domain-separated leaf and internal hash functions. They are the
733    // anchor that cross-SDK verifiers can compare against to detect
734    // split-view attacks.
735
736    #[test]
737    fn v2_leaf_uses_0x00_prefix() {
738        let got = hash_leaf_v2("art_test");
739        let mut h = Sha256::new();
740        h.update([0x00u8]);
741        h.update(b"art_test");
742        let expected: [u8; 32] = h.finalize().into();
743        assert_eq!(got, expected);
744    }
745
746    #[test]
747    fn v2_internal_uses_0x01_prefix() {
748        let left = [0x11u8; 32];
749        let right = [0x22u8; 32];
750        let got = hash_internal_v2(&left, &right);
751        let mut h = Sha256::new();
752        h.update([0x01u8]);
753        h.update(left);
754        h.update(right);
755        let expected: [u8; 32] = h.finalize().into();
756        assert_eq!(got, expected);
757    }
758
759    #[test]
760    fn v1_legacy_root_unchanged() {
761        // Pin a known v1 root so existing v0.10.2 receipts continue to
762        // verify byte-identically against this code path.
763        let ids = ["art_a", "art_b", "art_c", "art_d"];
764        let mut leaves: Vec<[u8; 32]> = ids.iter().map(|id| hash_leaf_v1(id)).collect();
765        while leaves.len() > 1 {
766            let mut next = Vec::with_capacity((leaves.len() + 1) / 2);
767            let mut i = 0;
768            while i + 1 < leaves.len() {
769                next.push(hash_internal_v1(&leaves[i], &leaves[i + 1]));
770                i += 2;
771            }
772            if i < leaves.len() {
773                next.push(leaves[i]);
774            }
775            leaves = next;
776        }
777        // This expected root is computed from the *current* v0.10.2
778        // hashing (no domain separation, RFC 9162 promotion). If this
779        // value changes, every v0.10.2 receipt in the wild becomes
780        // unverifiable — that would be the definition of a regression.
781        let got = hex::encode(leaves[0]);
782        // Pinned literal: v0.10.2 hashing applied to the input set above.
783        // Drift here means a previously-issued v0.10.2 receipt would no
784        // longer verify under v1.
785        let expected = "cb4c9e4a9374ea3917b9ba75554ce8908a593db1183f1af48edf41fa3eb67b0d";
786        assert_eq!(
787            got, expected,
788            "v1 root drifted; v0.10.2 receipts will fail to verify",
789        );
790    }
791
792    #[test]
793    fn v2_differs_from_v1_for_same_input() {
794        // Sanity: domain separation must change the output.
795        assert_ne!(hash_leaf_v1("x"), hash_leaf_v2("x"));
796        let l = [0x33u8; 32];
797        let r = [0x44u8; 32];
798        assert_ne!(hash_internal_v1(&l, &r), hash_internal_v2(&l, &r));
799    }
800
801    #[test]
802    fn odd_number_of_leaves() {
803        // 5 leaves -- last leaf is duplicated for padding
804        let mut tree = MerkleTree::new();
805        for i in 0..5 {
806            tree.append(&format!("art_{}", i));
807        }
808
809        let root = hex::encode(tree.root().unwrap());
810        let proof = tree.inclusion_proof(4).unwrap(); // last leaf
811
812        assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_4", &proof));
813    }
814
815    // ── v2 round-trip + cross-version rejection ────────────────────
816
817    #[test]
818    fn v2_verify_round_trip() {
819        let mut tree = MerkleTree::new();
820        for id in &["art_a", "art_b", "art_c", "art_d"] {
821            tree.append(id);
822        }
823        assert_eq!(tree.version(), MERKLE_VERSION_V2);
824
825        let root = hex::encode(tree.root().unwrap());
826        let proof = tree.inclusion_proof(1).unwrap();
827        assert_eq!(proof.merkle_version, MERKLE_VERSION_V2);
828        assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_b", &proof));
829    }
830
831    #[test]
832    fn v2_rejects_v1_proof() {
833        // Build a v2 tree, take its proof, then mutate the proof to claim
834        // v1. Re-verification under v2 (the trusted version) must fail
835        // because proof.merkle_version mismatches expected_version.
836        let mut tree = MerkleTree::new();
837        for id in &["art_a", "art_b", "art_c", "art_d"] {
838            tree.append(id);
839        }
840        let root = hex::encode(tree.root().unwrap());
841        let mut proof = tree.inclusion_proof(1).unwrap();
842        proof.merkle_version = MERKLE_VERSION_V1;
843        assert!(
844            !MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_b", &proof),
845            "v2 verifier must reject a proof that downgrades itself to v1",
846        );
847    }
848
849    #[test]
850    fn v1_rejects_v2_proof() {
851        // Symmetric to v2_rejects_v1_proof: a v1 tree's proof reinterpreted
852        // as v2 must reject.
853        let mut tree = MerkleTree::with_version(MERKLE_VERSION_V1).unwrap();
854        for id in &["art_a", "art_b", "art_c", "art_d"] {
855            tree.append(id);
856        }
857        let root = hex::encode(tree.root().unwrap());
858        let mut proof = tree.inclusion_proof(1).unwrap();
859        proof.merkle_version = MERKLE_VERSION_V2;
860        assert!(
861            !MerkleTree::verify_proof(MERKLE_VERSION_V1, &root, "art_b", &proof),
862            "v1 verifier must reject a proof that upgrades itself to v2",
863        );
864    }
865
866    #[test]
867    fn v2_rejects_internal_node_as_leaf() {
868        // Construct an internal-node hash (under v2 domain separation),
869        // hand-craft a fake "single-leaf proof" claiming this internal
870        // hash is the leaf. The verifier must reject because the
871        // computed v2 leaf hash for the supplied artifact_id cannot
872        // equal an internal-node hash (different domain prefix).
873        let left = [0x11u8; 32];
874        let right = [0x22u8; 32];
875        let internal = hash_internal_v2(&left, &right);
876        let internal_hex = hex::encode(internal);
877
878        let fake_proof = InclusionProof {
879            leaf_index: 0,
880            leaf_hash: internal_hex.clone(),
881            path: vec![],
882            algorithm: Some(MERKLE_ALGORITHM_V2.to_string()),
883            merkle_version: MERKLE_VERSION_V2,
884        };
885
886        assert!(
887            !MerkleTree::verify_proof(MERKLE_VERSION_V2, &internal_hex, "art_attacker", &fake_proof),
888            "v2 verifier must reject an internal-node hash impersonating a single-leaf tree",
889        );
890    }
891
892    #[test]
893    fn v2_rejects_empty_path_with_nonzero_leaf_index() {
894        // Empty proof path only makes sense for a single-leaf tree at
895        // leaf_index 0. A v2 verifier must reject any other shape.
896        let mut tree = MerkleTree::new();
897        tree.append("art_a");
898        let root = hex::encode(tree.root().unwrap());
899
900        // Synthesize a malformed proof: index 1 but no path.
901        let bad = InclusionProof {
902            leaf_index: 1,
903            leaf_hash: hex::encode(hash_leaf_v2("art_a")),
904            path: vec![],
905            algorithm: Some(MERKLE_ALGORITHM_V2.to_string()),
906            merkle_version: MERKLE_VERSION_V2,
907        };
908        assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &bad));
909    }
910
911    #[test]
912    fn unknown_merkle_version_rejected() {
913        let mut tree = MerkleTree::new();
914        tree.append("art_a");
915        tree.append("art_b");
916        let root = hex::encode(tree.root().unwrap());
917        let mut proof = tree.inclusion_proof(0).unwrap();
918        proof.merkle_version = 7; // not v1 or v2
919        // Trusted version is v2 here; proof claims 7 — must reject.
920        assert!(!MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &proof));
921        // And separately: if the *trusted* version is unknown, also reject.
922        let mut proof2 = tree.inclusion_proof(0).unwrap();
923        proof2.merkle_version = 7;
924        assert!(!MerkleTree::verify_proof(7, &root, "art_a", &proof2));
925    }
926
927    #[test]
928    fn default_merkle_version_function_returns_one() {
929        // Documents the contract relied on by `#[serde(default = ...)]`
930        // for backward compatibility with v0.10.2 receipts.
931        assert_eq!(default_merkle_version_v1(), MERKLE_VERSION_V1);
932    }
933
934    #[test]
935    fn missing_merkle_version_field_defaults_to_v1() {
936        // Deserialize an InclusionProof JSON produced before this field
937        // existed and confirm it falls back to v1.
938        let json = serde_json::json!({
939            "leaf_index": 0,
940            "leaf_hash": "00".repeat(32),
941            "path": [],
942            "algorithm": "sha256-duplicate-last",
943        });
944        let proof: InclusionProof = serde_json::from_value(json).unwrap();
945        assert_eq!(proof.merkle_version, MERKLE_VERSION_V1);
946    }
947
948    #[test]
949    fn mixed_version_in_receipt_explicit() {
950        // Round-trip a proof through JSON with `merkle_version: 2`
951        // explicitly set, then verify the deserialized proof routes
952        // through the v2 dispatch.
953        let mut tree = MerkleTree::new();
954        for id in &["art_a", "art_b"] {
955            tree.append(id);
956        }
957        let root = hex::encode(tree.root().unwrap());
958        let proof = tree.inclusion_proof(0).unwrap();
959        let json = serde_json::to_string(&proof).unwrap();
960        assert!(json.contains("\"merkle_version\":2"), "wire shape must include merkle_version=2");
961        let parsed: InclusionProof = serde_json::from_str(&json).unwrap();
962        assert_eq!(parsed.merkle_version, MERKLE_VERSION_V2);
963        assert!(MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_a", &parsed));
964    }
965
966    // ── Adversarial regression coverage ────────────────────────────
967    //
968    // These five tests anchor the fixes that closed the v1→v2 downgrade
969    // vector. Each one will FAIL if a regression undoes a corresponding
970    // guard (trusted-version dispatch, unknown-version rejection at
971    // construct/verify, per-proof version drift, canonical signing
972    // binding).
973
974    #[test]
975    fn cross_version_downgrade_rejected() {
976        // The exploit the original PR missed: a legit v2-signed
977        // checkpoint reused with a hand-crafted v1-flavored proof that
978        // claims merkle_version=1, empty path, leaf_hash = root,
979        // leaf_index = 0. Before the fix, verify_proof would dispatch
980        // through v1 hashing and (with the empty-path guard skipped)
981        // accept the forgery. After the fix, the trusted version comes
982        // from the *checkpoint* and proof.merkle_version != expected
983        // is a hard reject.
984        let mut tree = MerkleTree::new();
985        for id in &["art_a", "art_b"] {
986            tree.append(id);
987        }
988        let root = hex::encode(tree.root().unwrap());
989
990        // Craft the malicious single-leaf v1 proof.
991        let downgrade = InclusionProof {
992            leaf_index: 0,
993            leaf_hash: root.clone(),
994            path: vec![],
995            algorithm: Some(MERKLE_ALGORITHM_V1.to_string()),
996            merkle_version: MERKLE_VERSION_V1,
997        };
998
999        // The trusted version is v2 (taken from the v2-signed checkpoint
1000        // in real flows). Verification MUST fail on the version
1001        // mismatch — not silently dispatch through v1.
1002        assert!(
1003            !MerkleTree::verify_proof(MERKLE_VERSION_V2, &root, "art_attacker", &downgrade),
1004            "v1 proof must NOT verify when the trusted version is v2 (downgrade vector)",
1005        );
1006    }
1007
1008    #[test]
1009    fn unknown_merkle_version_rejected_at_construct() {
1010        // with_version must reject anything outside {1, 2}. This is the
1011        // invariant that lets `hash_leaf`/`hash_internal` keep returning
1012        // valid hashes once the tree exists.
1013        assert!(matches!(
1014            MerkleTree::with_version(99),
1015            Err(MerkleError::UnknownVersion(99)),
1016        ));
1017        assert!(matches!(
1018            MerkleTree::with_version(0),
1019            Err(MerkleError::UnknownVersion(0)),
1020        ));
1021        assert!(MerkleTree::with_version(MERKLE_VERSION_V1).is_ok());
1022        assert!(MerkleTree::with_version(MERKLE_VERSION_V2).is_ok());
1023    }
1024
1025    #[test]
1026    fn unknown_merkle_version_rejected_at_primitive() {
1027        // The hash primitives themselves reject unknown versions — no
1028        // silent fallback to v1. Defense in depth: even if a future
1029        // refactor bypasses with_version, the primitive layer holds.
1030        assert!(matches!(
1031            hash_leaf(42, "x"),
1032            Err(MerkleError::UnknownVersion(42)),
1033        ));
1034        let l = [0u8; 32];
1035        let r = [0u8; 32];
1036        assert!(matches!(
1037            hash_internal(42, &l, &r),
1038            Err(MerkleError::UnknownVersion(42)),
1039        ));
1040    }
1041
1042    #[test]
1043    fn checkpoint_canonical_includes_version() {
1044        // Sign two checkpoints with byte-identical (index, root,
1045        // tree_size, height, signer, signed_at) but differing
1046        // merkle_version. Swapping the signature between them must
1047        // fail verification — proving merkle_version is bound into
1048        // the canonical signing string. Without this binding, an
1049        // attacker could re-label a v2 checkpoint as v1 (or vice
1050        // versa) and keep the original signature.
1051        use crate::merkle::checkpoint::{
1052            Checkpoint, CANONICAL_VERSION_V1, CANONICAL_VERSION_V2, CANONICAL_VERSION_V3,
1053        };
1054        let canonical_v1 = Checkpoint::canonical_for_signing(
1055            CANONICAL_VERSION_V1,
1056            MERKLE_VERSION_V1,
1057            None,
1058            None,
1059            7,
1060            "sha256:abcd",
1061            42,
1062            6,
1063            "key_test",
1064            "2026-05-17T00:00:00Z",
1065        );
1066        let canonical_v2 = Checkpoint::canonical_for_signing(
1067            CANONICAL_VERSION_V2,
1068            MERKLE_VERSION_V2,
1069            None,
1070            None,
1071            7,
1072            "sha256:abcd",
1073            42,
1074            6,
1075            "key_test",
1076            "2026-05-17T00:00:00Z",
1077        );
1078        let canonical_v3 = Checkpoint::canonical_for_signing(
1079            CANONICAL_VERSION_V3,
1080            MERKLE_VERSION_V2,
1081            Some(MERKLE_ALGORITHM_V2),
1082            None,
1083            7,
1084            "sha256:abcd",
1085            42,
1086            6,
1087            "key_test",
1088            "2026-05-17T00:00:00Z",
1089        );
1090
1091        // The canonical strings must all differ. Equal would mean an
1092        // attacker can swap canonical or merkle versions freely.
1093        assert_ne!(
1094            canonical_v1, canonical_v2,
1095            "canonical strings for v1 vs v2 must differ — merkle_version must be bound into signing",
1096        );
1097        assert_ne!(
1098            canonical_v2, canonical_v3,
1099            "canonical strings for v2 vs v3 must differ — canonical_version must be bound into signing",
1100        );
1101        // Format tags so any third-party verifier reproducing the
1102        // string can detect (and dispatch on) the format in use.
1103        assert!(
1104            canonical_v2.starts_with("v2|"),
1105            "v2 canonical must be prefixed with v2| tag, got: {canonical_v2}",
1106        );
1107        assert!(
1108            canonical_v3.starts_with("v3|"),
1109            "v3 canonical must be prefixed with v3| tag, got: {canonical_v3}",
1110        );
1111        // The v1 form must remain byte-identical to the legacy format
1112        // so pre-v0.10.3 checkpoints continue verifying.
1113        assert_eq!(
1114            canonical_v1,
1115            "7|sha256:abcd|42|6|key_test|2026-05-17T00:00:00Z",
1116            "v1 canonical must remain byte-identical to legacy",
1117        );
1118    }
1119}