Skip to main content

curvy_core/
imt.rs

1//! Incremental Merkle Tree (arity 2, Poseidon hash) - a faithful port of
2//! `@zk-kit/imt`'s `IMT`, plus indexed and stateful sharded engines.
3//!
4//! The sharded tree cuts the depth-30 tree at `shard_height`: leaves below the cut
5//! live in fixed `2^shard_height`-leaf shards; the "cap" above is a small tree over
6//! the completed shard roots. With zero-padding this is *exactly* equal to the flat
7//! IMT over the same leaves. [`IndexedMerkleTree`] retains reverse lookup for the
8//! generic/full-tree use cases. [`ShardedNotesTree`] retains only the live shard,
9//! completed roots and owned paths. The stateless [`sharded_root`] and
10//! [`sharded_witness`] helpers remain parity oracles.
11
12use std::collections::{HashMap, HashSet};
13use std::fmt;
14use std::sync::LazyLock;
15
16use ark_ff::AdditiveGroup;
17#[cfg(feature = "parallel")]
18use rayon::prelude::*;
19
20use crate::field::{Fr, fr_from_be_32_checked, fr_to_be_32};
21use crate::poseidon::poseidon;
22
23/// Failure while mutating or restoring an incremental/sharded tree.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum TreeError {
26    InvalidGeometry { depth: usize, shard_height: usize },
27    CapacityOverflow { depth: usize },
28    TreeFull { depth: usize },
29    DuplicateLeaf,
30    LeafNotFound,
31    LeafIndexOutOfRange { index: usize, leaf_count: usize },
32    DuplicateOwnedLeaf { leaf_index: usize },
33    OwnedLeafMismatch { leaf_index: usize },
34    NoteNotMarked,
35    NoteAlreadyCompleted { shard_index: usize },
36    ShardNotCompleted { shard_index: usize },
37    InvalidSiblingCount { expected: usize, actual: usize },
38    WitnessRootMismatch { shard_index: usize },
39    InvalidSnapshot(String),
40    RewindBeforeCompleted { minimum: usize, requested: usize },
41    NonCanonicalField,
42}
43
44impl fmt::Display for TreeError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::InvalidGeometry {
48                depth,
49                shard_height,
50            } => {
51                write!(
52                    f,
53                    "sharded tree: shard height must be in (0, {depth}); got {shard_height}"
54                )
55            }
56            Self::CapacityOverflow { depth } => write!(
57                f,
58                "tree: depth {depth} does not fit this platform's index type"
59            ),
60            Self::TreeFull { depth } => write!(f, "tree: depth-{depth} tree is full"),
61            Self::DuplicateLeaf => write!(f, "tree: leaf already exists"),
62            Self::LeafNotFound => write!(f, "tree: leaf not found"),
63            Self::LeafIndexOutOfRange { index, leaf_count } => {
64                write!(
65                    f,
66                    "tree: leaf index {index} is out of range for {leaf_count} leaves"
67                )
68            }
69            Self::DuplicateOwnedLeaf { leaf_index } => {
70                write!(
71                    f,
72                    "sharded tree: more than one owned note is assigned to leaf {leaf_index}"
73                )
74            }
75            Self::OwnedLeafMismatch { leaf_index } => {
76                write!(
77                    f,
78                    "sharded tree: owned note does not match leaf {leaf_index}"
79                )
80            }
81            Self::NoteNotMarked => write!(f, "sharded tree: note is not marked"),
82            Self::NoteAlreadyCompleted { shard_index } => {
83                write!(
84                    f,
85                    "sharded tree: note is in completed shard {shard_index}; adopt a frozen witness"
86                )
87            }
88            Self::ShardNotCompleted { shard_index } => {
89                write!(f, "sharded tree: shard {shard_index} is not completed")
90            }
91            Self::InvalidSiblingCount { expected, actual } => {
92                write!(
93                    f,
94                    "sharded tree: expected {expected} within-shard siblings, got {actual}"
95                )
96            }
97            Self::WitnessRootMismatch { shard_index } => {
98                write!(
99                    f,
100                    "sharded tree: witness does not hash to shard {shard_index}'s root"
101                )
102            }
103            Self::InvalidSnapshot(message) => {
104                write!(f, "sharded tree: invalid snapshot: {message}")
105            }
106            Self::RewindBeforeCompleted { minimum, requested } => write!(
107                f,
108                "sharded tree: cannot rewind completed shards in place (minimum {minimum}, requested {requested}); restore a checkpoint",
109            ),
110            Self::NonCanonicalField => {
111                write!(
112                    f,
113                    "tree: value is not a canonical 32-byte BN254 field element"
114                )
115            }
116        }
117    }
118}
119
120impl std::error::Error for TreeError {}
121
122/// A depth-`d` inclusion proof: `siblings[level]` is the single sibling at each
123/// level (arity 2), `index` is the leaf's global position.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct InclusionProof {
126    pub leaf: Fr,
127    pub index: usize,
128    pub siblings: Vec<Fr>,
129    pub root: Fr,
130}
131
132/// Depth of the production Curvy notes tree.
133pub const NOTES_TREE_DEPTH: usize = 30;
134
135/// Shard height of the production Curvy notes tree: a shard covers `2^14` leaves.
136pub const NOTES_SHARD_HEIGHT: usize = 14;
137
138/// Leaves per completed shard in the production notes tree (`1 << NOTES_SHARD_HEIGHT`).
139pub const NOTES_SHARD_SIZE: usize = 1 << NOTES_SHARD_HEIGHT;
140
141/// Schema version of the persisted notes-tree state. Bump only when the
142/// persisted layout changes in a way that invalidates stored checkpoints.
143pub const NOTES_TREE_VERSION: u32 = 1;
144
145/// Largest depth served from the precomputed zero-root table. Every tree this
146/// crate can construct is covered: `tree_capacity` rejects depths whose
147/// `1 << depth` does not fit a `usize`.
148const MAX_CACHED_ZERO_DEPTH: usize = 64;
149
150/// The `Fr::ZERO`-leaf zero-root table, computed once.
151///
152/// `zero_roots_from` is a pure recurrence in which `z[i]` depends only on
153/// `z[i - 1]`, so the table for any depth is a *prefix* of the table for a
154/// larger depth. One table therefore serves every depth without changing a
155/// single hash.
156static ZERO_ROOTS: LazyLock<Vec<Fr>> =
157    LazyLock::new(|| zero_roots_from(MAX_CACHED_ZERO_DEPTH, Fr::ZERO));
158
159/// `Z[h]` = root of an all-empty subtree of height `h` (`Z[0] = 0`), for `h` in
160/// `0..=depth`. `Z[depth]` is the empty-tree root.
161///
162/// Served from a precomputed table, so this is a copy rather than `depth`
163/// Poseidon hashes. Restoring a depth-30 frontier was measured at 99.8%
164/// zero-root recomputation before this cache existed.
165pub fn zero_roots(depth: usize) -> Vec<Fr> {
166    match ZERO_ROOTS.get(..=depth) {
167        Some(prefix) => prefix.to_vec(),
168        None => zero_roots_from(depth, Fr::ZERO),
169    }
170}
171
172/// Zero roots starting from a caller-supplied leaf-level zero.
173pub fn zero_roots_from(depth: usize, zero_leaf: Fr) -> Vec<Fr> {
174    let mut z = Vec::with_capacity(depth + 1);
175    z.push(zero_leaf);
176    for _ in 0..depth {
177        let last = *z.last().unwrap();
178        z.push(poseidon(&[last, last]));
179    }
180    z
181}
182
183/// Incremental Merkle Tree, arity 2, Poseidon(left, right) node hash.
184#[derive(Clone)]
185pub struct Imt {
186    depth: usize,
187    zero_leaf: Fr,
188    zeroes: Vec<Fr>,     // zeroes[level], len depth
189    nodes: Vec<Vec<Fr>>, // nodes[level], len depth + 1
190}
191
192impl Imt {
193    /// An empty tree of the given depth. `root()` is the empty-tree root `Z[depth]`.
194    pub fn new(depth: usize) -> Self {
195        Self::new_with_zero(depth, Fr::ZERO)
196    }
197
198    /// An empty tree whose leaf-level zero is `zero_leaf`.
199    ///
200    /// The sharded tree uses this for its cap: an empty cap leaf represents an
201    /// entire empty shard, so its zero is `Z[shard_height]`, not field zero.
202    pub fn new_with_zero(depth: usize, zero_leaf: Fr) -> Self {
203        let z = zero_roots_from(depth, zero_leaf);
204        let mut nodes = vec![Vec::new(); depth + 1];
205        nodes[depth] = vec![z[depth]]; // root of the empty tree
206        Self {
207            depth,
208            zero_leaf,
209            zeroes: z[..depth].to_vec(),
210            nodes,
211        }
212    }
213
214    /// Bulk-build from an ordered leaf log (O(n) hashes), like the `IMT` constructor.
215    pub fn from_leaves(depth: usize, leaves: &[Fr]) -> Self {
216        Self::from_leaves_with_zero(depth, Fr::ZERO, leaves)
217    }
218
219    /// Bulk-build with a caller-supplied leaf-level zero value.
220    pub fn from_leaves_with_zero(depth: usize, zero_leaf: Fr, leaves: &[Fr]) -> Self {
221        let mut t = Self::new_with_zero(depth, zero_leaf);
222        if leaves.is_empty() {
223            return t;
224        }
225        t.nodes[0] = leaves.to_vec();
226        for level in 0..depth {
227            t.nodes[level + 1] = build_parent_level(&t.nodes[level], t.zeroes[level]);
228        }
229        t
230    }
231
232    /// Append one leaf (incremental, O(depth) hashes). Produces the same tree as
233    /// [`Self::from_leaves`] over the same leaf sequence.
234    pub fn insert(&mut self, leaf: Fr) {
235        debug_assert!(self.nodes[0].len() < self.capacity().unwrap_or(usize::MAX));
236        let mut node = leaf;
237        let mut index = self.nodes[0].len();
238        for level in 0..self.depth {
239            let pos = index % 2;
240            let start = index - pos;
241            // place `node` at nodes[level][index] (index == current len for an append)
242            if index < self.nodes[level].len() {
243                self.nodes[level][index] = node;
244            } else {
245                self.nodes[level].resize(index, self.zeroes[level]);
246                self.nodes[level].push(node);
247            }
248            let left = self.nodes[level]
249                .get(start)
250                .copied()
251                .unwrap_or(self.zeroes[level]);
252            let right = self.nodes[level]
253                .get(start + 1)
254                .copied()
255                .unwrap_or(self.zeroes[level]);
256            node = poseidon(&[left, right]);
257            index /= 2;
258        }
259        if self.nodes[self.depth].is_empty() {
260            self.nodes[self.depth].push(node);
261        } else {
262            self.nodes[self.depth][0] = node;
263        }
264    }
265
266    pub fn leaf_count(&self) -> usize {
267        self.nodes[0].len()
268    }
269
270    pub fn capacity(&self) -> Result<usize, TreeError> {
271        1usize
272            .checked_shl(self.depth as u32)
273            .ok_or(TreeError::CapacityOverflow { depth: self.depth })
274    }
275
276    pub fn leaf(&self, index: usize) -> Option<Fr> {
277        self.nodes[0].get(index).copied()
278    }
279
280    /// Replace an existing leaf and re-hash only its path to the root.
281    pub fn update(&mut self, index: usize, leaf: Fr) -> Result<(), TreeError> {
282        let leaf_count = self.leaf_count();
283        if index >= leaf_count {
284            return Err(TreeError::LeafIndexOutOfRange { index, leaf_count });
285        }
286
287        self.nodes[0][index] = leaf;
288        let mut node_index = index;
289        for level in 0..self.depth {
290            let pair_start = node_index & !1;
291            let left = self.nodes[level]
292                .get(pair_start)
293                .copied()
294                .unwrap_or(self.zeroes[level]);
295            let right = self.nodes[level]
296                .get(pair_start + 1)
297                .copied()
298                .unwrap_or(self.zeroes[level]);
299            node_index >>= 1;
300            self.nodes[level + 1][node_index] = poseidon(&[left, right]);
301        }
302        Ok(())
303    }
304
305    /// Drop a suffix of leaves and bulk-rebuild the remaining tree.
306    pub fn truncate(&mut self, leaf_count: usize) -> Result<(), TreeError> {
307        let current = self.leaf_count();
308        if leaf_count > current {
309            return Err(TreeError::LeafIndexOutOfRange {
310                index: leaf_count,
311                leaf_count: current,
312            });
313        }
314        let leaves = self.nodes[0][..leaf_count].to_vec();
315        *self = Self::from_leaves_with_zero(self.depth, self.zero_leaf, &leaves);
316        Ok(())
317    }
318
319    pub fn root(&self) -> Fr {
320        self.nodes[self.depth][0]
321    }
322
323    /// Inclusion proof for the leaf at `index` (`depth` siblings, bottom→top).
324    pub fn create_proof(&self, index: usize) -> InclusionProof {
325        assert!(index < self.nodes[0].len(), "imt: leaf index out of range");
326        let leaf = self.nodes[0][index];
327        let mut siblings = Vec::with_capacity(self.depth);
328        let mut idx = index;
329        for level in 0..self.depth {
330            let sib_i = if idx.is_multiple_of(2) {
331                idx + 1
332            } else {
333                idx - 1
334            };
335            let sib = self.nodes[level]
336                .get(sib_i)
337                .copied()
338                .unwrap_or(self.zeroes[level]);
339            siblings.push(sib);
340            idx /= 2;
341        }
342        InclusionProof {
343            leaf,
344            index,
345            siblings,
346            root: self.root(),
347        }
348    }
349}
350
351const FRONTIER_SNAPSHOT_MAGIC: &[u8; 8] = b"CVYFRONT";
352const FRONTIER_SNAPSHOT_VERSION: u8 = 1;
353const FRONTIER_SNAPSHOT_HEADER_LEN: usize = 24;
354
355/// A completed fixed-height subtree emitted while appending to [`NotesFrontier`].
356#[derive(Clone, Debug, PartialEq, Eq)]
357pub struct CompletedShard {
358    pub shard_index: usize,
359    pub root: Fr,
360}
361
362/// Result of one append to [`NotesFrontier`].
363#[derive(Clone, Debug, PartialEq, Eq)]
364pub struct FrontierAppend {
365    pub leaf_index: usize,
366    pub completed_shard: Option<CompletedShard>,
367}
368
369/// Constant-space append frontier.
370///
371/// Unlike [`ShardedNotesTree`], this type retains no leaves, cap, reverse index,
372/// or owned-note witnesses. `frontier[level]` is the completed subtree covering
373/// the rightmost set bit of `leaf_count`; the optional slot at `depth` holds the
374/// root only when the tree is completely full. A depth-30 snapshot is at most
375/// 1,015 bytes and is therefore cheap to persist once per hot block for reorg
376/// rollback.
377#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct NotesFrontier {
379    depth: usize,
380    shard_height: usize,
381    shard_size: usize,
382    leaf_count: usize,
383    frontier: Vec<Option<Fr>>,
384    zeroes: Vec<Fr>,
385}
386
387impl NotesFrontier {
388    pub fn new(depth: usize, shard_height: usize) -> Result<Self, TreeError> {
389        if shard_height == 0 || shard_height >= depth {
390            return Err(TreeError::InvalidGeometry {
391                depth,
392                shard_height,
393            });
394        }
395        let shard_size = tree_capacity(shard_height)?;
396        tree_capacity(depth)?;
397        Ok(Self {
398            depth,
399            shard_height,
400            shard_size,
401            leaf_count: 0,
402            frontier: vec![None; depth + 1],
403            zeroes: zero_roots(depth),
404        })
405    }
406
407    /// An empty frontier with the production notes-tree geometry
408    /// ([`NOTES_TREE_DEPTH`] / [`NOTES_SHARD_HEIGHT`]).
409    ///
410    /// Infallible: the protocol geometry is a compile-time constant that
411    /// [`NotesFrontier::new`] accepts, which `production_geometry_is_valid`
412    /// asserts.
413    pub fn production() -> Self {
414        Self::new(NOTES_TREE_DEPTH, NOTES_SHARD_HEIGHT)
415            .expect("production notes-tree geometry is valid")
416    }
417
418    pub fn depth(&self) -> usize {
419        self.depth
420    }
421
422    pub fn shard_height(&self) -> usize {
423        self.shard_height
424    }
425
426    pub fn shard_size(&self) -> usize {
427        self.shard_size
428    }
429
430    pub fn leaf_count(&self) -> usize {
431        self.leaf_count
432    }
433
434    /// Number of *completed* shards: `leaf_count >> shard_height`.
435    ///
436    /// A partially filled trailing shard is not counted, so this is exactly the
437    /// number of shard roots emitted by [`NotesFrontier::append`] so far.
438    pub fn shard_count(&self) -> usize {
439        self.leaf_count >> self.shard_height
440    }
441
442    /// The depth-`depth` root using the protocol's recursive zero padding.
443    pub fn root(&self) -> Fr {
444        let capacity = tree_capacity(self.depth).unwrap_or(usize::MAX);
445        if self.leaf_count == capacity {
446            return self.frontier[self.depth].unwrap_or(self.zeroes[self.depth]);
447        }
448
449        let mut node = self.zeroes[0];
450        let mut occupied = self.leaf_count;
451        for level in 0..self.depth {
452            node = if occupied & 1 == 1 {
453                let left = self.frontier[level].unwrap_or(self.zeroes[level]);
454                poseidon(&[left, node])
455            } else {
456                poseidon(&[node, self.zeroes[level]])
457            };
458            occupied >>= 1;
459        }
460        node
461    }
462
463    /// Append one leaf in O(depth), returning an emitted shard root exactly when
464    /// this leaf completes a `2^shard_height` subtree.
465    pub fn append(&mut self, leaf: Fr) -> Result<FrontierAppend, TreeError> {
466        let capacity = tree_capacity(self.depth)?;
467        if self.leaf_count >= capacity {
468            return Err(TreeError::TreeFull { depth: self.depth });
469        }
470
471        let leaf_index = self.leaf_count;
472        let mut cursor = leaf_index;
473        let mut node = leaf;
474        let mut completed_shard = None;
475
476        for level in 0..=self.depth {
477            if cursor & 1 == 0 {
478                self.frontier[level] = Some(node);
479                break;
480            }
481
482            let left = self.frontier[level].take().ok_or_else(|| {
483                TreeError::InvalidSnapshot(format!(
484                    "frontier level {level} is empty for occupied leaf-count bit",
485                ))
486            })?;
487            node = poseidon(&[left, node]);
488            cursor >>= 1;
489
490            if level + 1 == self.shard_height {
491                completed_shard = Some(CompletedShard {
492                    shard_index: leaf_index >> self.shard_height,
493                    root: node,
494                });
495            }
496        }
497
498        self.leaf_count += 1;
499        Ok(FrontierAppend {
500            leaf_index,
501            completed_shard,
502        })
503    }
504
505    /// Append a packed logical batch atomically with respect to capacity checks.
506    /// Only completed shard descriptors are returned; leaf indices remain dense
507    /// from the pre-append `leaf_count`.
508    pub fn append_many(&mut self, leaves: &[Fr]) -> Result<Vec<CompletedShard>, TreeError> {
509        let capacity = tree_capacity(self.depth)?;
510        if leaves.len() > capacity.saturating_sub(self.leaf_count) {
511            return Err(TreeError::TreeFull { depth: self.depth });
512        }
513
514        let mut completed = Vec::new();
515        for leaf in leaves {
516            if let Some(shard) = self.append(*leaf)?.completed_shard {
517                completed.push(shard);
518            }
519        }
520        Ok(completed)
521    }
522
523    /// [`NotesFrontier::append`] over a canonical big-endian 32-byte leaf.
524    ///
525    /// Every consumer reaching this type across a byte boundary - the wasm/TS
526    /// boundary otherwise repeats the same `fr_from_be_32_checked` marshalling.
527    /// Rejects non-canonical encodings rather than reducing them into the field.
528    pub fn append_be_32(&mut self, leaf: &[u8; 32]) -> Result<FrontierAppend, TreeError> {
529        let leaf = fr_from_be_32_checked(leaf).ok_or(TreeError::NonCanonicalField)?;
530        self.append(leaf)
531    }
532
533    /// [`NotesFrontier::root`] as canonical big-endian 32 bytes.
534    pub fn root_be_32(&self) -> [u8; 32] {
535        fr_to_be_32(&self.root())
536    }
537
538    /// Canonical versioned snapshot suitable for a per-block database checkpoint.
539    pub fn encode_snapshot(&self) -> Vec<u8> {
540        let present = self.frontier.iter().filter(|slot| slot.is_some()).count();
541        let mut bytes =
542            Vec::with_capacity(FRONTIER_SNAPSHOT_HEADER_LEN + self.frontier.len() + present * 32);
543        bytes.extend_from_slice(FRONTIER_SNAPSHOT_MAGIC);
544        bytes.push(FRONTIER_SNAPSHOT_VERSION);
545        bytes.push(self.depth as u8);
546        bytes.push(self.shard_height as u8);
547        bytes.push(0);
548        bytes.extend_from_slice(&(self.leaf_count as u64).to_be_bytes());
549        bytes.extend_from_slice(&(self.frontier.len() as u32).to_be_bytes());
550        for slot in &self.frontier {
551            match slot {
552                Some(field) => {
553                    bytes.push(1);
554                    bytes.extend_from_slice(&fr_to_be_32(field));
555                }
556                None => bytes.push(0),
557            }
558        }
559        bytes
560    }
561
562    pub fn from_snapshot_bytes(bytes: &[u8]) -> Result<Self, TreeError> {
563        if bytes.len() < FRONTIER_SNAPSHOT_HEADER_LEN {
564            return Err(TreeError::InvalidSnapshot(
565                "frontier snapshot header is truncated".to_owned(),
566            ));
567        }
568        if bytes.get(..8) != Some(FRONTIER_SNAPSHOT_MAGIC.as_slice()) {
569            return Err(TreeError::InvalidSnapshot(
570                "invalid frontier snapshot magic".to_owned(),
571            ));
572        }
573        if bytes[8] != FRONTIER_SNAPSHOT_VERSION {
574            return Err(TreeError::InvalidSnapshot(format!(
575                "unsupported frontier snapshot version {}",
576                bytes[8],
577            )));
578        }
579        if bytes[11] != 0 {
580            return Err(TreeError::InvalidSnapshot(
581                "frontier snapshot reserved byte is non-zero".to_owned(),
582            ));
583        }
584
585        let depth = bytes[9] as usize;
586        let shard_height = bytes[10] as usize;
587        let raw_leaf_count: [u8; 8] = bytes[12..20]
588            .try_into()
589            .map_err(|_| TreeError::InvalidSnapshot("invalid frontier leaf count".to_owned()))?;
590        let leaf_count = usize::try_from(u64::from_be_bytes(raw_leaf_count)).map_err(|_| {
591            TreeError::InvalidSnapshot("frontier leaf count exceeds this platform".to_owned())
592        })?;
593        let raw_slot_count: [u8; 4] = bytes[20..24]
594            .try_into()
595            .map_err(|_| TreeError::InvalidSnapshot("invalid frontier slot count".to_owned()))?;
596        let slot_count = u32::from_be_bytes(raw_slot_count) as usize;
597
598        let mut tree = Self::new(depth, shard_height)?;
599        let capacity = tree_capacity(depth)?;
600        if leaf_count > capacity {
601            return Err(TreeError::InvalidSnapshot(format!(
602                "frontier leaf count {leaf_count} exceeds capacity {capacity}",
603            )));
604        }
605        if slot_count != depth + 1 {
606            return Err(TreeError::InvalidSnapshot(format!(
607                "frontier snapshot has {slot_count} slots; expected {}",
608                depth + 1,
609            )));
610        }
611
612        let mut cursor = FRONTIER_SNAPSHOT_HEADER_LEN;
613        for level in 0..slot_count {
614            let flag = *bytes.get(cursor).ok_or_else(|| {
615                TreeError::InvalidSnapshot("truncated frontier slot flag".to_owned())
616            })?;
617            cursor += 1;
618            tree.frontier[level] = match flag {
619                0 => None,
620                1 => Some(read_field(bytes, &mut cursor)?),
621                _ => {
622                    return Err(TreeError::InvalidSnapshot(format!(
623                        "invalid frontier slot flag {flag}",
624                    )));
625                }
626            };
627
628            let should_be_present = (leaf_count >> level) & 1 == 1;
629            if tree.frontier[level].is_some() != should_be_present {
630                return Err(TreeError::InvalidSnapshot(format!(
631                    "frontier slot {level} does not match leaf count {leaf_count}",
632                )));
633            }
634        }
635        if cursor != bytes.len() {
636            return Err(TreeError::InvalidSnapshot(
637                "trailing frontier snapshot bytes".to_owned(),
638            ));
639        }
640
641        tree.leaf_count = leaf_count;
642        Ok(tree)
643    }
644}
645
646fn build_parent_level(level: &[Fr], zero: Fr) -> Vec<Fr> {
647    #[cfg(feature = "parallel")]
648    {
649        level
650            .par_chunks(2)
651            .map(|pair| poseidon(&[pair[0], pair.get(1).copied().unwrap_or(zero)]))
652            .collect()
653    }
654    #[cfg(not(feature = "parallel"))]
655    {
656        level
657            .chunks(2)
658            .map(|pair| poseidon(&[pair[0], pair.get(1).copied().unwrap_or(zero)]))
659            .collect()
660    }
661}
662
663/// Incremental Merkle tree with a reverse leaf index.
664///
665/// The sharded wallet path normally uses [`ShardedNotesTree`]; this indexed form
666/// is for cold-shard recovery, pending-note witness generation and the full-tree
667/// profile.
668#[derive(Clone)]
669pub struct IndexedMerkleTree {
670    tree: Imt,
671    indices: HashMap<Fr, usize>,
672}
673
674/// Position-addressed Merkle tree that deliberately permits duplicate leaves.
675///
676/// Gas-fee trees are keyed by token index rather than leaf value, so two tokens
677/// may legitimately have the same fee. Keep this separate from
678/// [`IndexedMerkleTree`] so note trees retain their duplicate-rejection invariant.
679#[derive(Clone)]
680pub struct OrderedMerkleTree {
681    tree: Imt,
682}
683
684impl OrderedMerkleTree {
685    pub fn new(depth: usize) -> Result<Self, TreeError> {
686        tree_capacity(depth)?;
687        Ok(Self {
688            tree: Imt::new(depth),
689        })
690    }
691
692    pub fn from_leaves(depth: usize, leaves: &[Fr]) -> Result<Self, TreeError> {
693        let capacity = tree_capacity(depth)?;
694        if leaves.len() > capacity {
695            return Err(TreeError::TreeFull { depth });
696        }
697        Ok(Self {
698            tree: Imt::from_leaves(depth, leaves),
699        })
700    }
701
702    pub fn depth(&self) -> usize {
703        self.tree.depth
704    }
705
706    pub fn leaf_count(&self) -> usize {
707        self.tree.leaf_count()
708    }
709
710    pub fn root(&self) -> Fr {
711        self.tree.root()
712    }
713
714    pub fn insert(&mut self, leaf: Fr) -> Result<usize, TreeError> {
715        if self.leaf_count() >= self.tree.capacity()? {
716            return Err(TreeError::TreeFull {
717                depth: self.depth(),
718            });
719        }
720        let index = self.leaf_count();
721        self.tree.insert(leaf);
722        Ok(index)
723    }
724
725    pub fn insert_many(&mut self, leaves: &[Fr]) -> Result<(), TreeError> {
726        if leaves.len() > self.tree.capacity()?.saturating_sub(self.leaf_count()) {
727            return Err(TreeError::TreeFull {
728                depth: self.depth(),
729            });
730        }
731        for leaf in leaves {
732            self.tree.insert(*leaf);
733        }
734        Ok(())
735    }
736
737    pub fn create_proof_at(&self, index: usize) -> Result<InclusionProof, TreeError> {
738        if index >= self.leaf_count() {
739            return Err(TreeError::LeafIndexOutOfRange {
740                index,
741                leaf_count: self.leaf_count(),
742            });
743        }
744        Ok(self.tree.create_proof(index))
745    }
746}
747
748impl IndexedMerkleTree {
749    pub fn new(depth: usize) -> Result<Self, TreeError> {
750        tree_capacity(depth)?;
751        Ok(Self {
752            tree: Imt::new(depth),
753            indices: HashMap::new(),
754        })
755    }
756
757    pub fn from_leaves(depth: usize, leaves: &[Fr]) -> Result<Self, TreeError> {
758        let capacity = tree_capacity(depth)?;
759        if leaves.len() > capacity {
760            return Err(TreeError::TreeFull { depth });
761        }
762        let mut indices = HashMap::with_capacity(leaves.len());
763        for (index, leaf) in leaves.iter().copied().enumerate() {
764            if indices.insert(leaf, index).is_some() {
765                return Err(TreeError::DuplicateLeaf);
766            }
767        }
768        Ok(Self {
769            tree: Imt::from_leaves(depth, leaves),
770            indices,
771        })
772    }
773
774    pub fn depth(&self) -> usize {
775        self.tree.depth
776    }
777
778    pub fn leaf_count(&self) -> usize {
779        self.tree.leaf_count()
780    }
781
782    pub fn leaves(&self) -> &[Fr] {
783        &self.tree.nodes[0]
784    }
785
786    pub fn root(&self) -> Fr {
787        self.tree.root()
788    }
789
790    pub fn get_index(&self, leaf: Fr) -> Option<usize> {
791        self.indices.get(&leaf).copied()
792    }
793
794    pub fn insert(&mut self, leaf: Fr) -> Result<usize, TreeError> {
795        if self.indices.contains_key(&leaf) {
796            return Err(TreeError::DuplicateLeaf);
797        }
798        if self.leaf_count() >= self.tree.capacity()? {
799            return Err(TreeError::TreeFull {
800                depth: self.depth(),
801            });
802        }
803        let index = self.leaf_count();
804        self.tree.insert(leaf);
805        self.indices.insert(leaf, index);
806        Ok(index)
807    }
808
809    pub fn insert_many(&mut self, leaves: &[Fr]) -> Result<(), TreeError> {
810        if leaves.len() > self.tree.capacity()?.saturating_sub(self.leaf_count()) {
811            return Err(TreeError::TreeFull {
812                depth: self.depth(),
813            });
814        }
815        let mut incoming = HashSet::with_capacity(leaves.len());
816        for leaf in leaves {
817            if self.indices.contains_key(leaf) || !incoming.insert(*leaf) {
818                return Err(TreeError::DuplicateLeaf);
819            }
820        }
821        for leaf in leaves {
822            self.insert(*leaf)?;
823        }
824        Ok(())
825    }
826
827    pub fn create_proof(&self, leaf: Fr) -> Result<InclusionProof, TreeError> {
828        let index = self.get_index(leaf).ok_or(TreeError::LeafNotFound)?;
829        Ok(self.tree.create_proof(index))
830    }
831
832    pub fn create_proof_at(&self, index: usize) -> Result<InclusionProof, TreeError> {
833        if index >= self.leaf_count() {
834            return Err(TreeError::LeafIndexOutOfRange {
835                index,
836                leaf_count: self.leaf_count(),
837            });
838        }
839        Ok(self.tree.create_proof(index))
840    }
841
842    pub fn truncate(&mut self, leaf_count: usize) -> Result<(), TreeError> {
843        if leaf_count > self.leaf_count() {
844            return Err(TreeError::LeafIndexOutOfRange {
845                index: leaf_count,
846                leaf_count: self.leaf_count(),
847            });
848        }
849        let removed: Vec<Fr> = self.leaves()[leaf_count..].to_vec();
850        self.tree.truncate(leaf_count)?;
851        for leaf in removed {
852            self.indices.remove(&leaf);
853        }
854        Ok(())
855    }
856}
857
858/// Verify an inclusion proof (bottom→top, `index` bit selects sibling side).
859pub fn verify_proof(proof: &InclusionProof) -> bool {
860    let mut node = proof.leaf;
861    let mut idx = proof.index;
862    for &sib in &proof.siblings {
863        node = if idx.is_multiple_of(2) {
864            poseidon(&[node, sib])
865        } else {
866            poseidon(&[sib, node])
867        };
868        idx >>= 1;
869    }
870    node == proof.root
871}
872
873// ── stateful sharded tree (bounded live shard + mutable cap) ────────────────
874
875/// Persisted witness state for one owned note.
876#[derive(Clone, Debug, PartialEq, Eq)]
877pub struct OwnedNoteWitness {
878    pub note_id: Fr,
879    pub leaf_index: usize,
880    /// Frozen once the note's shard completes; derived from the live tree before then.
881    pub within_shard_siblings: Option<Vec<Fr>>,
882}
883
884/// Minimal state needed to restore a [`ShardedNotesTree`].
885#[derive(Clone, Debug, PartialEq, Eq)]
886pub struct ShardedTreeSnapshot {
887    pub depth: usize,
888    pub shard_height: usize,
889    pub completed_roots: Vec<Fr>,
890    pub live_leaves: Vec<Fr>,
891    pub owned_notes: Vec<OwnedNoteWitness>,
892}
893
894const SNAPSHOT_MAGIC: [u8; 4] = *b"CYST";
895const SNAPSHOT_VERSION: u8 = 1;
896const SNAPSHOT_HEADER_LEN: usize = 20;
897
898impl ShardedTreeSnapshot {
899    /// Encode a deterministic, versioned binary snapshot.
900    ///
901    /// Field elements use canonical 32-byte big-endian encoding. Chain identity,
902    /// block hash and sync cursor intentionally remain storage-layer metadata, so
903    /// this blob can be embedded in any caller's checkpoint format.
904    pub fn encode(&self) -> Result<Vec<u8>, TreeError> {
905        if self.shard_height == 0 || self.shard_height >= self.depth {
906            return Err(TreeError::InvalidGeometry {
907                depth: self.depth,
908                shard_height: self.shard_height,
909            });
910        }
911        let shard_size = tree_capacity(self.shard_height)?;
912        let max_shards = tree_capacity(self.depth - self.shard_height)?;
913        if self.completed_roots.len() > max_shards {
914            return Err(TreeError::InvalidSnapshot(
915                "too many completed shard roots".to_owned(),
916            ));
917        }
918        if self.live_leaves.len() >= shard_size {
919            return Err(TreeError::InvalidSnapshot(
920                "the live shard must contain fewer than one complete shard".to_owned(),
921            ));
922        }
923        if self.completed_roots.len() == max_shards && !self.live_leaves.is_empty() {
924            return Err(TreeError::InvalidSnapshot(
925                "live leaves exceed the tree capacity".to_owned(),
926            ));
927        }
928        let depth: u8 = self.depth.try_into().map_err(|_| {
929            TreeError::InvalidSnapshot("depth does not fit the snapshot format".to_owned())
930        })?;
931        let shard_height: u8 = self.shard_height.try_into().map_err(|_| {
932            TreeError::InvalidSnapshot("shard height does not fit the snapshot format".to_owned())
933        })?;
934        let completed_count: u32 = self
935            .completed_roots
936            .len()
937            .try_into()
938            .map_err(|_| TreeError::InvalidSnapshot("too many completed roots".to_owned()))?;
939        let live_count: u32 = self
940            .live_leaves
941            .len()
942            .try_into()
943            .map_err(|_| TreeError::InvalidSnapshot("too many live leaves".to_owned()))?;
944        let owned_count: u32 = self
945            .owned_notes
946            .len()
947            .try_into()
948            .map_err(|_| TreeError::InvalidSnapshot("too many owned notes".to_owned()))?;
949
950        let siblings_count: usize = self
951            .owned_notes
952            .iter()
953            .map(|owned| owned.within_shard_siblings.as_ref().map_or(0, Vec::len))
954            .sum();
955        let fields_count = self
956            .completed_roots
957            .len()
958            .checked_add(self.live_leaves.len())
959            .and_then(|count| count.checked_add(self.owned_notes.len()))
960            .and_then(|count| count.checked_add(siblings_count))
961            .ok_or_else(|| TreeError::InvalidSnapshot("snapshot size overflow".to_owned()))?;
962        let owned_metadata_bytes = self
963            .owned_notes
964            .len()
965            .checked_mul(8)
966            .ok_or_else(|| TreeError::InvalidSnapshot("snapshot size overflow".to_owned()))?;
967        let byte_capacity =
968            SNAPSHOT_HEADER_LEN
969                .checked_add(fields_count.checked_mul(32).ok_or_else(|| {
970                    TreeError::InvalidSnapshot("snapshot size overflow".to_owned())
971                })?)
972                .and_then(|size| size.checked_add(owned_metadata_bytes))
973                .ok_or_else(|| TreeError::InvalidSnapshot("snapshot size overflow".to_owned()))?;
974        let mut bytes = Vec::with_capacity(byte_capacity);
975        bytes.extend_from_slice(&SNAPSHOT_MAGIC);
976        bytes.extend_from_slice(&[SNAPSHOT_VERSION, depth, shard_height, 0]);
977        bytes.extend_from_slice(&completed_count.to_be_bytes());
978        bytes.extend_from_slice(&live_count.to_be_bytes());
979        bytes.extend_from_slice(&owned_count.to_be_bytes());
980        append_fields(&mut bytes, &self.completed_roots);
981        append_fields(&mut bytes, &self.live_leaves);
982
983        for owned in &self.owned_notes {
984            bytes.extend_from_slice(&fr_to_be_32(&owned.note_id));
985            let leaf_index: u32 = owned.leaf_index.try_into().map_err(|_| {
986                TreeError::InvalidSnapshot("leaf index does not fit u32".to_owned())
987            })?;
988            bytes.extend_from_slice(&leaf_index.to_be_bytes());
989            bytes.push(u8::from(owned.within_shard_siblings.is_some()));
990            bytes.extend_from_slice(&[0; 3]);
991            if let Some(siblings) = &owned.within_shard_siblings {
992                if siblings.len() != self.shard_height {
993                    return Err(TreeError::InvalidSiblingCount {
994                        expected: self.shard_height,
995                        actual: siblings.len(),
996                    });
997                }
998                append_fields(&mut bytes, siblings);
999            }
1000        }
1001        Ok(bytes)
1002    }
1003
1004    pub fn decode(bytes: &[u8]) -> Result<Self, TreeError> {
1005        if bytes.len() < SNAPSHOT_HEADER_LEN {
1006            return Err(TreeError::InvalidSnapshot("truncated header".to_owned()));
1007        }
1008        if bytes[..4] != SNAPSHOT_MAGIC {
1009            return Err(TreeError::InvalidSnapshot("bad magic".to_owned()));
1010        }
1011        if bytes[4] != SNAPSHOT_VERSION {
1012            return Err(TreeError::InvalidSnapshot(format!(
1013                "unsupported version {}",
1014                bytes[4],
1015            )));
1016        }
1017        if bytes[7] != 0 {
1018            return Err(TreeError::InvalidSnapshot(
1019                "reserved header byte is non-zero".to_owned(),
1020            ));
1021        }
1022
1023        let depth = usize::from(bytes[5]);
1024        let shard_height = usize::from(bytes[6]);
1025        if shard_height == 0 || shard_height >= depth {
1026            return Err(TreeError::InvalidGeometry {
1027                depth,
1028                shard_height,
1029            });
1030        }
1031        let completed_count = read_u32(bytes, 8)? as usize;
1032        let live_count = read_u32(bytes, 12)? as usize;
1033        let owned_count = read_u32(bytes, 16)? as usize;
1034        let shard_size = tree_capacity(shard_height)?;
1035        let max_shards = tree_capacity(depth - shard_height)?;
1036        if completed_count > max_shards {
1037            return Err(TreeError::InvalidSnapshot(
1038                "too many completed shard roots".to_owned(),
1039            ));
1040        }
1041        if live_count >= shard_size {
1042            return Err(TreeError::InvalidSnapshot(
1043                "the live shard must contain fewer than one complete shard".to_owned(),
1044            ));
1045        }
1046        if completed_count == max_shards && live_count != 0 {
1047            return Err(TreeError::InvalidSnapshot(
1048                "live leaves exceed the tree capacity".to_owned(),
1049            ));
1050        }
1051
1052        let fixed_field_count = completed_count
1053            .checked_add(live_count)
1054            .ok_or_else(|| TreeError::InvalidSnapshot("field count overflow".to_owned()))?;
1055        let fixed_bytes = fixed_field_count
1056            .checked_mul(32)
1057            .ok_or_else(|| TreeError::InvalidSnapshot("field byte count overflow".to_owned()))?;
1058        if fixed_bytes > bytes.len() - SNAPSHOT_HEADER_LEN {
1059            return Err(TreeError::InvalidSnapshot(
1060                "truncated root/leaf fields".to_owned(),
1061            ));
1062        }
1063        let minimum_owned_bytes = owned_count.checked_mul(40).ok_or_else(|| {
1064            TreeError::InvalidSnapshot("owned-note byte count overflow".to_owned())
1065        })?;
1066        if minimum_owned_bytes > bytes.len() - SNAPSHOT_HEADER_LEN - fixed_bytes {
1067            return Err(TreeError::InvalidSnapshot(
1068                "truncated owned-note records".to_owned(),
1069            ));
1070        }
1071
1072        let mut cursor = SNAPSHOT_HEADER_LEN;
1073        let completed_roots = read_fields(bytes, &mut cursor, completed_count)?;
1074        let live_leaves = read_fields(bytes, &mut cursor, live_count)?;
1075        let mut owned_notes = Vec::with_capacity(owned_count);
1076        for _ in 0..owned_count {
1077            let note_id = read_field(bytes, &mut cursor)?;
1078            let leaf_index = read_u32_at_cursor(bytes, &mut cursor)? as usize;
1079            let flag = *bytes.get(cursor).ok_or_else(|| {
1080                TreeError::InvalidSnapshot("truncated owned-note flag".to_owned())
1081            })?;
1082            cursor += 1;
1083            let reserved = bytes.get(cursor..cursor + 3).ok_or_else(|| {
1084                TreeError::InvalidSnapshot("truncated owned-note padding".to_owned())
1085            })?;
1086            if reserved != [0, 0, 0] {
1087                return Err(TreeError::InvalidSnapshot(
1088                    "owned-note padding is non-zero".to_owned(),
1089                ));
1090            }
1091            cursor += 3;
1092            let within_shard_siblings = match flag {
1093                0 => None,
1094                1 => Some(read_fields(bytes, &mut cursor, shard_height)?),
1095                _ => {
1096                    return Err(TreeError::InvalidSnapshot(format!(
1097                        "invalid frozen-path flag {flag}"
1098                    )));
1099                }
1100            };
1101            owned_notes.push(OwnedNoteWitness {
1102                note_id,
1103                leaf_index,
1104                within_shard_siblings,
1105            });
1106        }
1107        if cursor != bytes.len() {
1108            return Err(TreeError::InvalidSnapshot("trailing bytes".to_owned()));
1109        }
1110
1111        Ok(Self {
1112            depth,
1113            shard_height,
1114            completed_roots,
1115            live_leaves,
1116            owned_notes,
1117        })
1118    }
1119}
1120
1121fn append_fields(bytes: &mut Vec<u8>, fields: &[Fr]) {
1122    for field in fields {
1123        bytes.extend_from_slice(&fr_to_be_32(field));
1124    }
1125}
1126
1127fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, TreeError> {
1128    let raw: [u8; 4] = bytes
1129        .get(offset..offset + 4)
1130        .ok_or_else(|| TreeError::InvalidSnapshot("truncated u32".to_owned()))?
1131        .try_into()
1132        .map_err(|_| TreeError::InvalidSnapshot("invalid u32".to_owned()))?;
1133    Ok(u32::from_be_bytes(raw))
1134}
1135
1136fn read_u32_at_cursor(bytes: &[u8], cursor: &mut usize) -> Result<u32, TreeError> {
1137    let value = read_u32(bytes, *cursor)?;
1138    *cursor += 4;
1139    Ok(value)
1140}
1141
1142fn read_field(bytes: &[u8], cursor: &mut usize) -> Result<Fr, TreeError> {
1143    let raw = bytes
1144        .get(*cursor..*cursor + 32)
1145        .ok_or_else(|| TreeError::InvalidSnapshot("truncated field element".to_owned()))?;
1146    let field = fr_from_be_32_checked(raw)
1147        .ok_or_else(|| TreeError::InvalidSnapshot("non-canonical field element".to_owned()))?;
1148    *cursor += 32;
1149    Ok(field)
1150}
1151
1152fn read_fields(bytes: &[u8], cursor: &mut usize, count: usize) -> Result<Vec<Fr>, TreeError> {
1153    let mut fields = Vec::with_capacity(count);
1154    for _ in 0..count {
1155        fields.push(read_field(bytes, cursor)?);
1156    }
1157    Ok(fields)
1158}
1159
1160/// Stateful depth-`depth` notes tree split into fixed-height shards.
1161///
1162/// Only the rightmost shard stores leaves and internal nodes. Completed shards
1163/// are represented by one root each in a mutable cap tree. Owned notes retain
1164/// their immutable within-shard path after rollover; the current cap path is
1165/// attached on demand, producing a normal depth-`depth` inclusion proof.
1166#[derive(Clone)]
1167pub struct ShardedNotesTree {
1168    depth: usize,
1169    shard_height: usize,
1170    shard_size: usize,
1171    cap_depth: usize,
1172    completed_roots: Vec<Fr>,
1173    live: Imt,
1174    live_leaves: Vec<Fr>,
1175    cap: Imt,
1176    owned_notes: HashMap<Fr, OwnedNoteWitness>,
1177    dirty_owned_notes: HashSet<Fr>,
1178}
1179
1180impl ShardedNotesTree {
1181    pub fn new(depth: usize, shard_height: usize) -> Result<Self, TreeError> {
1182        if shard_height == 0 || shard_height >= depth {
1183            return Err(TreeError::InvalidGeometry {
1184                depth,
1185                shard_height,
1186            });
1187        }
1188        let shard_size = tree_capacity(shard_height)?;
1189        let cap_depth = depth - shard_height;
1190        tree_capacity(depth)?;
1191        let global_zeroes = zero_roots(depth);
1192
1193        Ok(Self {
1194            depth,
1195            shard_height,
1196            shard_size,
1197            cap_depth,
1198            completed_roots: Vec::new(),
1199            live: Imt::new(shard_height),
1200            live_leaves: Vec::new(),
1201            cap: Imt::new_with_zero(cap_depth, global_zeroes[shard_height]),
1202            owned_notes: HashMap::new(),
1203            dirty_owned_notes: HashSet::new(),
1204        })
1205    }
1206
1207    pub fn from_snapshot(snapshot: ShardedTreeSnapshot) -> Result<Self, TreeError> {
1208        let mut tree = Self::new(snapshot.depth, snapshot.shard_height)?;
1209        let max_shards = tree_capacity(tree.cap_depth)?;
1210        if snapshot.completed_roots.len() > max_shards {
1211            return Err(TreeError::InvalidSnapshot(
1212                "too many completed shard roots".to_owned(),
1213            ));
1214        }
1215        if snapshot.live_leaves.len() >= tree.shard_size {
1216            return Err(TreeError::InvalidSnapshot(
1217                "the live shard must contain fewer than one complete shard".to_owned(),
1218            ));
1219        }
1220        if snapshot.completed_roots.len() == max_shards && !snapshot.live_leaves.is_empty() {
1221            return Err(TreeError::InvalidSnapshot(
1222                "live leaves exceed the tree capacity".to_owned(),
1223            ));
1224        }
1225
1226        tree.completed_roots = snapshot.completed_roots;
1227        tree.live_leaves = snapshot.live_leaves;
1228        tree.live = Imt::from_leaves(tree.shard_height, &tree.live_leaves);
1229        tree.rebuild_cap();
1230
1231        let capacity = tree_capacity(tree.depth)?;
1232        for owned in snapshot.owned_notes {
1233            if owned.leaf_index >= capacity {
1234                return Err(TreeError::InvalidSnapshot(format!(
1235                    "owned leaf {} exceeds capacity {capacity}",
1236                    owned.leaf_index,
1237                )));
1238            }
1239            if tree.owned_notes.contains_key(&owned.note_id)
1240                || tree
1241                    .owned_notes
1242                    .values()
1243                    .any(|existing| existing.leaf_index == owned.leaf_index)
1244            {
1245                return Err(TreeError::InvalidSnapshot(format!(
1246                    "duplicate owned note or leaf {}",
1247                    owned.leaf_index,
1248                )));
1249            }
1250
1251            let shard_index = owned.leaf_index >> tree.shard_height;
1252            if shard_index < tree.completed_roots.len() {
1253                let siblings = owned.within_shard_siblings.as_ref().ok_or_else(|| {
1254                    TreeError::InvalidSnapshot(format!(
1255                        "owned leaf {} is completed but has no frozen path",
1256                        owned.leaf_index,
1257                    ))
1258                })?;
1259                tree.verify_frozen_path(owned.note_id, owned.leaf_index, siblings)?;
1260            } else {
1261                if owned.within_shard_siblings.is_some() {
1262                    return Err(TreeError::InvalidSnapshot(format!(
1263                        "owned leaf {} is not completed but has a frozen path",
1264                        owned.leaf_index,
1265                    )));
1266                }
1267                if owned.leaf_index < tree.leaf_count()
1268                    && tree.live.leaf(owned.leaf_index & (tree.shard_size - 1))
1269                        != Some(owned.note_id)
1270                {
1271                    return Err(TreeError::OwnedLeafMismatch {
1272                        leaf_index: owned.leaf_index,
1273                    });
1274                }
1275            }
1276            tree.owned_notes.insert(owned.note_id, owned);
1277        }
1278
1279        Ok(tree)
1280    }
1281
1282    /// Restore the public tree state before owned witnesses are adopted. This is
1283    /// the bulk boundary used by storage adapters that persist shard roots, live
1284    /// leaves, and account-scoped witnesses in separate tables.
1285    pub fn from_parts(
1286        depth: usize,
1287        shard_height: usize,
1288        completed_roots: Vec<Fr>,
1289        live_leaves: Vec<Fr>,
1290    ) -> Result<Self, TreeError> {
1291        Self::from_snapshot(ShardedTreeSnapshot {
1292            depth,
1293            shard_height,
1294            completed_roots,
1295            live_leaves,
1296            owned_notes: Vec::new(),
1297        })
1298    }
1299
1300    pub fn depth(&self) -> usize {
1301        self.depth
1302    }
1303
1304    pub fn shard_height(&self) -> usize {
1305        self.shard_height
1306    }
1307
1308    pub fn shard_size(&self) -> usize {
1309        self.shard_size
1310    }
1311
1312    pub fn leaf_count(&self) -> usize {
1313        self.completed_roots.len() * self.shard_size + self.live_leaves.len()
1314    }
1315
1316    pub fn completed_shard_count(&self) -> usize {
1317        self.completed_roots.len()
1318    }
1319
1320    pub fn owned_note_count(&self) -> usize {
1321        self.owned_notes.len()
1322    }
1323
1324    pub fn owned_notes(&self) -> Vec<OwnedNoteWitness> {
1325        let mut owned_notes: Vec<OwnedNoteWitness> = self.owned_notes.values().cloned().collect();
1326        owned_notes.sort_by_key(|owned| owned.leaf_index);
1327        owned_notes
1328    }
1329
1330    pub fn completed_roots(&self) -> &[Fr] {
1331        &self.completed_roots
1332    }
1333
1334    pub fn live_leaves(&self) -> &[Fr] {
1335        &self.live_leaves
1336    }
1337
1338    pub fn completed_shard_root(&self, shard_index: usize) -> Result<Fr, TreeError> {
1339        self.completed_roots
1340            .get(shard_index)
1341            .copied()
1342            .ok_or(TreeError::ShardNotCompleted { shard_index })
1343    }
1344
1345    /// Append one note commitment, updating the live shard and cap in O(depth).
1346    pub fn append(&mut self, note_id: Fr) -> Result<(), TreeError> {
1347        self.ensure_room(1)?;
1348        self.validate_incoming_owned_note(self.leaf_count(), note_id)?;
1349        self.live.insert(note_id);
1350        self.live_leaves.push(note_id);
1351        self.sync_live_root()?;
1352        if self.live_leaves.len() == self.shard_size {
1353            self.roll_over()?;
1354        }
1355        Ok(())
1356    }
1357
1358    /// Append a batch, bulk-building when that is cheaper than one insertion per
1359    /// leaf. Cap updates are coalesced to once per affected shard.
1360    pub fn append_many(&mut self, note_ids: &[Fr]) -> Result<(), TreeError> {
1361        self.ensure_room(note_ids.len())?;
1362        let first_index = self.leaf_count();
1363        for (offset, note_id) in note_ids.iter().copied().enumerate() {
1364            self.validate_incoming_owned_note(first_index + offset, note_id)?;
1365        }
1366
1367        let mut remaining = note_ids;
1368        while !remaining.is_empty() {
1369            let room = self.shard_size - self.live_leaves.len();
1370            let take = room.min(remaining.len());
1371            let segment = &remaining[..take];
1372            let final_live_len = self.live_leaves.len() + segment.len();
1373            let rebuild_cost = final_live_len.saturating_sub(1);
1374            let incremental_cost = segment.len().saturating_mul(self.shard_height);
1375
1376            self.live_leaves.extend_from_slice(segment);
1377            if rebuild_cost < incremental_cost {
1378                self.live = Imt::from_leaves(self.shard_height, &self.live_leaves);
1379            } else {
1380                for note_id in segment {
1381                    self.live.insert(*note_id);
1382                }
1383            }
1384            self.sync_live_root()?;
1385            if self.live_leaves.len() == self.shard_size {
1386                self.roll_over()?;
1387            }
1388            remaining = &remaining[take..];
1389        }
1390        Ok(())
1391    }
1392
1393    /// Track an owned note. The note may already be in the live shard or may be
1394    /// marked immediately before its commitment is appended.
1395    pub fn mark_owned(&mut self, note_id: Fr, leaf_index: usize) -> Result<(), TreeError> {
1396        let capacity = tree_capacity(self.depth)?;
1397        if leaf_index >= capacity {
1398            return Err(TreeError::LeafIndexOutOfRange {
1399                index: leaf_index,
1400                leaf_count: capacity,
1401            });
1402        }
1403        let shard_index = leaf_index >> self.shard_height;
1404        if shard_index < self.completed_roots.len() {
1405            return Err(TreeError::NoteAlreadyCompleted { shard_index });
1406        }
1407        if let Some(existing) = self.owned_notes.get(&note_id) {
1408            if existing.leaf_index == leaf_index {
1409                return Ok(());
1410            }
1411            return Err(TreeError::DuplicateOwnedLeaf {
1412                leaf_index: existing.leaf_index,
1413            });
1414        }
1415        if self
1416            .owned_notes
1417            .values()
1418            .any(|owned| owned.note_id != note_id && owned.leaf_index == leaf_index)
1419        {
1420            return Err(TreeError::DuplicateOwnedLeaf { leaf_index });
1421        }
1422        if leaf_index < self.leaf_count()
1423            && self.live.leaf(leaf_index & (self.shard_size - 1)) != Some(note_id)
1424        {
1425            return Err(TreeError::OwnedLeafMismatch { leaf_index });
1426        }
1427
1428        self.owned_notes.insert(
1429            note_id,
1430            OwnedNoteWitness {
1431                note_id,
1432                leaf_index,
1433                within_shard_siblings: None,
1434            },
1435        );
1436        self.dirty_owned_notes.insert(note_id);
1437        Ok(())
1438    }
1439
1440    pub fn unmark_owned(&mut self, note_id: Fr) -> bool {
1441        self.dirty_owned_notes.remove(&note_id);
1442        self.owned_notes.remove(&note_id).is_some()
1443    }
1444
1445    /// Adopt a recovered local path for an owned note in a completed shard.
1446    pub fn adopt_frozen_witness(
1447        &mut self,
1448        note_id: Fr,
1449        leaf_index: usize,
1450        within_shard_siblings: Vec<Fr>,
1451    ) -> Result<(), TreeError> {
1452        let shard_index = leaf_index >> self.shard_height;
1453        if shard_index >= self.completed_roots.len() {
1454            return Err(TreeError::ShardNotCompleted { shard_index });
1455        }
1456        if let Some(existing) = self.owned_notes.get(&note_id)
1457            && existing.leaf_index != leaf_index
1458        {
1459            return Err(TreeError::DuplicateOwnedLeaf {
1460                leaf_index: existing.leaf_index,
1461            });
1462        }
1463        if self
1464            .owned_notes
1465            .values()
1466            .any(|owned| owned.note_id != note_id && owned.leaf_index == leaf_index)
1467        {
1468            return Err(TreeError::DuplicateOwnedLeaf { leaf_index });
1469        }
1470        self.verify_frozen_path(note_id, leaf_index, &within_shard_siblings)?;
1471        self.owned_notes.insert(
1472            note_id,
1473            OwnedNoteWitness {
1474                note_id,
1475                leaf_index,
1476                within_shard_siblings: Some(within_shard_siblings),
1477            },
1478        );
1479        self.dirty_owned_notes.insert(note_id);
1480        Ok(())
1481    }
1482
1483    pub fn root(&self) -> Fr {
1484        self.cap.root()
1485    }
1486
1487    /// Produce a conventional full-depth proof for a tracked owned note.
1488    pub fn witness(&self, note_id: Fr) -> Result<InclusionProof, TreeError> {
1489        let owned = self
1490            .owned_notes
1491            .get(&note_id)
1492            .ok_or(TreeError::NoteNotMarked)?;
1493        let shard_index = owned.leaf_index >> self.shard_height;
1494        let within_index = owned.leaf_index & (self.shard_size - 1);
1495
1496        let mut siblings = if let Some(frozen) = &owned.within_shard_siblings {
1497            frozen.clone()
1498        } else {
1499            if shard_index != self.completed_roots.len()
1500                || self.live.leaf(within_index) != Some(note_id)
1501            {
1502                return Err(TreeError::OwnedLeafMismatch {
1503                    leaf_index: owned.leaf_index,
1504                });
1505            }
1506            self.live.create_proof(within_index).siblings
1507        };
1508
1509        let cap_proof = self.cap.create_proof(shard_index);
1510        siblings.extend(cap_proof.siblings);
1511        Ok(InclusionProof {
1512            leaf: note_id,
1513            index: owned.leaf_index,
1514            siblings,
1515            root: self.root(),
1516        })
1517    }
1518
1519    /// Rewind within the mutable live shard. Crossing a completed-shard boundary
1520    /// requires restoring an earlier checkpoint because completed leaves have
1521    /// deliberately been discarded.
1522    pub fn rewind_live_to(&mut self, leaf_count: usize) -> Result<Vec<Fr>, TreeError> {
1523        let completed_leaf_count = self.completed_roots.len() * self.shard_size;
1524        if leaf_count < completed_leaf_count {
1525            return Err(TreeError::RewindBeforeCompleted {
1526                minimum: completed_leaf_count,
1527                requested: leaf_count,
1528            });
1529        }
1530        let current = self.leaf_count();
1531        if leaf_count > current {
1532            return Err(TreeError::LeafIndexOutOfRange {
1533                index: leaf_count,
1534                leaf_count: current,
1535            });
1536        }
1537
1538        let removed: Vec<Fr> = self
1539            .owned_notes
1540            .values()
1541            .filter(|owned| owned.leaf_index >= leaf_count)
1542            .map(|owned| owned.note_id)
1543            .collect();
1544        for note_id in &removed {
1545            self.owned_notes.remove(note_id);
1546            self.dirty_owned_notes.remove(note_id);
1547        }
1548
1549        self.live_leaves.truncate(leaf_count - completed_leaf_count);
1550        self.live = Imt::from_leaves(self.shard_height, &self.live_leaves);
1551        self.rebuild_cap();
1552        Ok(removed)
1553    }
1554
1555    pub fn drain_dirty_owned_notes(&mut self) -> Vec<OwnedNoteWitness> {
1556        let mut dirty: Vec<OwnedNoteWitness> = self
1557            .dirty_owned_notes
1558            .drain()
1559            .filter_map(|note_id| self.owned_notes.get(&note_id).cloned())
1560            .collect();
1561        dirty.sort_by_key(|owned| owned.leaf_index);
1562        dirty
1563    }
1564
1565    pub fn snapshot(&self) -> ShardedTreeSnapshot {
1566        ShardedTreeSnapshot {
1567            depth: self.depth,
1568            shard_height: self.shard_height,
1569            completed_roots: self.completed_roots.clone(),
1570            live_leaves: self.live_leaves.clone(),
1571            owned_notes: self.owned_notes(),
1572        }
1573    }
1574
1575    pub fn encode_snapshot(&self) -> Result<Vec<u8>, TreeError> {
1576        self.snapshot().encode()
1577    }
1578
1579    pub fn from_snapshot_bytes(bytes: &[u8]) -> Result<Self, TreeError> {
1580        Self::from_snapshot(ShardedTreeSnapshot::decode(bytes)?)
1581    }
1582
1583    fn ensure_room(&self, additional: usize) -> Result<(), TreeError> {
1584        let capacity = tree_capacity(self.depth)?;
1585        if additional > capacity.saturating_sub(self.leaf_count()) {
1586            return Err(TreeError::TreeFull { depth: self.depth });
1587        }
1588        Ok(())
1589    }
1590
1591    fn validate_incoming_owned_note(
1592        &self,
1593        leaf_index: usize,
1594        note_id: Fr,
1595    ) -> Result<(), TreeError> {
1596        if self
1597            .owned_notes
1598            .values()
1599            .any(|owned| owned.leaf_index == leaf_index && owned.note_id != note_id)
1600        {
1601            return Err(TreeError::OwnedLeafMismatch { leaf_index });
1602        }
1603        Ok(())
1604    }
1605
1606    fn sync_live_root(&mut self) -> Result<(), TreeError> {
1607        if self.live_leaves.is_empty() {
1608            return Ok(());
1609        }
1610        let shard_index = self.completed_roots.len();
1611        if self.cap.leaf_count() == shard_index {
1612            self.cap.insert(self.live.root());
1613        } else if self.cap.leaf_count() == shard_index + 1 {
1614            self.cap.update(shard_index, self.live.root())?;
1615        } else {
1616            return Err(TreeError::InvalidSnapshot(
1617                "cap/live shard index mismatch".to_owned(),
1618            ));
1619        }
1620        Ok(())
1621    }
1622
1623    fn roll_over(&mut self) -> Result<(), TreeError> {
1624        let completed_index = self.completed_roots.len();
1625        for owned in self.owned_notes.values_mut() {
1626            if (owned.leaf_index >> self.shard_height) != completed_index
1627                || owned.within_shard_siblings.is_some()
1628            {
1629                continue;
1630            }
1631            let within_index = owned.leaf_index & (self.shard_size - 1);
1632            if self.live.leaf(within_index) != Some(owned.note_id) {
1633                return Err(TreeError::OwnedLeafMismatch {
1634                    leaf_index: owned.leaf_index,
1635                });
1636            }
1637            owned.within_shard_siblings = Some(self.live.create_proof(within_index).siblings);
1638            self.dirty_owned_notes.insert(owned.note_id);
1639        }
1640
1641        self.completed_roots.push(self.live.root());
1642        self.live = Imt::new(self.shard_height);
1643        self.live_leaves.clear();
1644        Ok(())
1645    }
1646
1647    fn verify_frozen_path(
1648        &self,
1649        note_id: Fr,
1650        leaf_index: usize,
1651        siblings: &[Fr],
1652    ) -> Result<(), TreeError> {
1653        if siblings.len() != self.shard_height {
1654            return Err(TreeError::InvalidSiblingCount {
1655                expected: self.shard_height,
1656                actual: siblings.len(),
1657            });
1658        }
1659        let shard_index = leaf_index >> self.shard_height;
1660        let root = self
1661            .completed_roots
1662            .get(shard_index)
1663            .copied()
1664            .ok_or(TreeError::ShardNotCompleted { shard_index })?;
1665        let mut node = note_id;
1666        let mut index = leaf_index & (self.shard_size - 1);
1667        for sibling in siblings {
1668            node = if index.is_multiple_of(2) {
1669                poseidon(&[node, *sibling])
1670            } else {
1671                poseidon(&[*sibling, node])
1672            };
1673            index >>= 1;
1674        }
1675        if node != root {
1676            return Err(TreeError::WitnessRootMismatch { shard_index });
1677        }
1678        Ok(())
1679    }
1680
1681    fn rebuild_cap(&mut self) {
1682        let cap_zero = zero_roots(self.depth)[self.shard_height];
1683        let mut roots = self.completed_roots.clone();
1684        if !self.live_leaves.is_empty() {
1685            roots.push(self.live.root());
1686        }
1687        self.cap = Imt::from_leaves_with_zero(self.cap_depth, cap_zero, &roots);
1688    }
1689}
1690
1691fn tree_capacity(depth: usize) -> Result<usize, TreeError> {
1692    1usize
1693        .checked_shl(depth as u32)
1694        .ok_or(TreeError::CapacityOverflow { depth })
1695}
1696
1697// ── sharded decomposition (stateless; equals the flat IMT over the same leaves) ──
1698
1699/// The cap level 0: one root per `2^shard_height`-leaf shard (the last shard may be
1700/// partial / "live").
1701fn shard_roots(leaves: &[Fr], shard_height: usize) -> Vec<Fr> {
1702    let shard_size = 1usize << shard_height;
1703    leaves
1704        .chunks(shard_size)
1705        .map(|chunk| Imt::from_leaves(shard_height, chunk).root())
1706        .collect()
1707}
1708
1709/// Fold the cap up `cap_depth` levels, padding with `Z[shard_height + k]`. Returns
1710/// every level (level 0 = shard roots, top = global root).
1711fn cap_levels(
1712    shard_roots: Vec<Fr>,
1713    z: &[Fr],
1714    shard_height: usize,
1715    cap_depth: usize,
1716) -> Vec<Vec<Fr>> {
1717    let mut levels = vec![shard_roots];
1718    for k in 0..cap_depth {
1719        let h = shard_height + k;
1720        let level = &levels[k];
1721        let mut next = Vec::with_capacity(level.len().div_ceil(2));
1722        let mut i = 0;
1723        while i < level.len() {
1724            let left = level[i];
1725            let right = if i + 1 < level.len() {
1726                level[i + 1]
1727            } else {
1728                z[h]
1729            };
1730            next.push(poseidon(&[left, right]));
1731            i += 2;
1732        }
1733        levels.push(next);
1734    }
1735    levels
1736}
1737
1738/// The global root of the sharded tree - equals `Imt::from_leaves(depth, leaves).root()`.
1739pub fn sharded_root(leaves: &[Fr], depth: usize, shard_height: usize) -> Fr {
1740    let z = zero_roots(depth);
1741    let cap_depth = depth - shard_height;
1742    let levels = cap_levels(
1743        shard_roots(leaves, shard_height),
1744        &z,
1745        shard_height,
1746        cap_depth,
1747    );
1748    let top = &levels[cap_depth];
1749    if top.is_empty() { z[depth] } else { top[0] }
1750}
1751
1752/// The full depth-`depth` inclusion proof for the leaf at `leaf_index`: the
1753/// within-shard siblings glued to the shared cap path - equals the flat IMT proof.
1754pub fn sharded_witness(
1755    leaves: &[Fr],
1756    leaf_index: usize,
1757    depth: usize,
1758    shard_height: usize,
1759) -> InclusionProof {
1760    let z = zero_roots(depth);
1761    let shard_size = 1usize << shard_height;
1762    let cap_depth = depth - shard_height;
1763    let shard_index = leaf_index >> shard_height;
1764    let within_index = leaf_index & (shard_size - 1);
1765
1766    let shard_start = shard_index * shard_size;
1767    let shard_end = (shard_start + shard_size).min(leaves.len());
1768    let shard_leaves = &leaves[shard_start..shard_end];
1769    let mut siblings = Imt::from_leaves(shard_height, shard_leaves)
1770        .create_proof(within_index)
1771        .siblings;
1772
1773    let levels = cap_levels(
1774        shard_roots(leaves, shard_height),
1775        &z,
1776        shard_height,
1777        cap_depth,
1778    );
1779    let mut idx = shard_index;
1780    for k in 0..cap_depth {
1781        let row = &levels[k];
1782        let sib = idx ^ 1;
1783        siblings.push(if sib < row.len() {
1784            row[sib]
1785        } else {
1786            z[shard_height + k]
1787        });
1788        idx >>= 1;
1789    }
1790
1791    let top = &levels[cap_depth];
1792    InclusionProof {
1793        leaf: leaves[leaf_index],
1794        index: leaf_index,
1795        siblings,
1796        root: if top.is_empty() { z[depth] } else { top[0] },
1797    }
1798}
1799
1800#[cfg(test)]
1801mod tests {
1802    use super::*;
1803    use crate::field::fr_from_dec;
1804
1805    #[test]
1806    fn empty_tree_root_depth_30() {
1807        // The protocol's EMPTY_TREE_ROOT (hash(0,0) folded 30×).
1808        assert_eq!(
1809            Imt::new(30).root(),
1810            fr_from_dec(
1811                "4114686047564160449611603615418567457008101555090703535405891656262658644463"
1812            ),
1813        );
1814    }
1815
1816    #[test]
1817    fn insert_matches_from_leaves_and_proof_verifies() {
1818        let leaves: Vec<Fr> = (1u64..=11).map(Fr::from).collect();
1819        let mut incremental = Imt::new(6);
1820        for &l in &leaves {
1821            incremental.insert(l);
1822        }
1823        let bulk = Imt::from_leaves(6, &leaves);
1824        assert_eq!(incremental.root(), bulk.root());
1825        for i in 0..leaves.len() {
1826            let p = bulk.create_proof(i);
1827            assert!(verify_proof(&p));
1828            assert_eq!(p, incremental.create_proof(i));
1829        }
1830    }
1831
1832    #[test]
1833    fn sharded_equals_flat() {
1834        let leaves: Vec<Fr> = (1u64..=20).map(Fr::from).collect();
1835        let (depth, shard_height) = (6, 2); // shard size 4 -> 5 shards
1836        let flat = Imt::from_leaves(depth, &leaves);
1837        assert_eq!(sharded_root(&leaves, depth, shard_height), flat.root());
1838        for i in 0..leaves.len() {
1839            assert_eq!(
1840                sharded_witness(&leaves, i, depth, shard_height),
1841                flat.create_proof(i)
1842            );
1843        }
1844    }
1845
1846    #[test]
1847    fn stateful_sharded_tree_matches_flat_across_rollovers() {
1848        let leaves: Vec<Fr> = (1u64..=21).map(Fr::from).collect();
1849        let (depth, shard_height) = (8, 3);
1850        let owned_indices = [2usize, 9, 19];
1851        let mut sharded = ShardedNotesTree::new(depth, shard_height).unwrap();
1852        for index in owned_indices {
1853            sharded.mark_owned(leaves[index], index).unwrap();
1854        }
1855        sharded.append_many(&leaves).unwrap();
1856
1857        let flat = Imt::from_leaves(depth, &leaves);
1858        assert_eq!(sharded.root(), flat.root());
1859        assert_eq!(sharded.completed_shard_count(), 2);
1860        assert_eq!(sharded.live_leaves(), &leaves[16..]);
1861        for index in owned_indices {
1862            assert_eq!(
1863                sharded.witness(leaves[index]).unwrap(),
1864                flat.create_proof(index)
1865            );
1866        }
1867
1868        let snapshot = sharded.snapshot();
1869        assert!(snapshot.owned_notes[0].within_shard_siblings.is_some());
1870        assert!(snapshot.owned_notes[1].within_shard_siblings.is_some());
1871        assert!(snapshot.owned_notes[2].within_shard_siblings.is_none());
1872    }
1873
1874    #[test]
1875    fn stateful_snapshot_roundtrip_preserves_roots_and_witnesses() {
1876        let leaves: Vec<Fr> = (1u64..=19).map(Fr::from).collect();
1877        let mut original = ShardedNotesTree::new(8, 3).unwrap();
1878        original.mark_owned(leaves[1], 1).unwrap();
1879        original.mark_owned(leaves[17], 17).unwrap();
1880        original.append_many(&leaves).unwrap();
1881
1882        let restored = ShardedNotesTree::from_snapshot(original.snapshot()).unwrap();
1883        assert_eq!(restored.root(), original.root());
1884        assert_eq!(restored.snapshot(), original.snapshot());
1885        assert_eq!(
1886            restored.witness(leaves[1]).unwrap(),
1887            original.witness(leaves[1]).unwrap()
1888        );
1889        assert_eq!(
1890            restored.witness(leaves[17]).unwrap(),
1891            original.witness(leaves[17]).unwrap()
1892        );
1893
1894        let encoded = original.encode_snapshot().unwrap();
1895        let decoded = ShardedNotesTree::from_snapshot_bytes(&encoded).unwrap();
1896        assert_eq!(decoded.snapshot(), original.snapshot());
1897        let mut corrupt = encoded;
1898        corrupt[0] ^= 1;
1899        assert!(matches!(
1900            ShardedNotesTree::from_snapshot_bytes(&corrupt),
1901            Err(TreeError::InvalidSnapshot(_))
1902        ));
1903    }
1904
1905    #[test]
1906    fn recovered_frozen_witness_is_verified_before_adoption() {
1907        let leaves: Vec<Fr> = (1u64..=11).map(Fr::from).collect();
1908        let mut sharded = ShardedNotesTree::new(8, 3).unwrap();
1909        sharded.append_many(&leaves).unwrap();
1910
1911        let local = Imt::from_leaves(3, &leaves[..8]).create_proof(3).siblings;
1912        sharded
1913            .adopt_frozen_witness(leaves[3], 3, local.clone())
1914            .unwrap();
1915        assert_eq!(
1916            sharded.witness(leaves[3]).unwrap(),
1917            Imt::from_leaves(8, &leaves).create_proof(3)
1918        );
1919
1920        let mut bad = Imt::from_leaves(3, &leaves[..8]).create_proof(2).siblings;
1921        bad[0] += Fr::from(1u64);
1922        assert_eq!(
1923            sharded.adopt_frozen_witness(leaves[2], 2, bad),
1924            Err(TreeError::WitnessRootMismatch { shard_index: 0 }),
1925        );
1926    }
1927
1928    #[test]
1929    fn batch_append_and_single_append_are_identical() {
1930        let leaves: Vec<Fr> = (1u64..=37).map(Fr::from).collect();
1931        let mut batch = ShardedNotesTree::new(10, 4).unwrap();
1932        batch.append_many(&leaves).unwrap();
1933
1934        let mut single = ShardedNotesTree::new(10, 4).unwrap();
1935        for leaf in &leaves {
1936            single.append(*leaf).unwrap();
1937        }
1938        assert_eq!(batch.snapshot(), single.snapshot());
1939        assert_eq!(batch.root(), Imt::from_leaves(10, &leaves).root());
1940    }
1941
1942    #[test]
1943    fn rewind_is_bounded_to_the_live_shard() {
1944        let leaves: Vec<Fr> = (1u64..=19).map(Fr::from).collect();
1945        let mut sharded = ShardedNotesTree::new(8, 3).unwrap();
1946        sharded.mark_owned(leaves[17], 17).unwrap();
1947        sharded.append_many(&leaves).unwrap();
1948
1949        assert_eq!(sharded.rewind_live_to(16).unwrap(), vec![leaves[17]]);
1950        assert_eq!(sharded.root(), Imt::from_leaves(8, &leaves[..16]).root());
1951        assert_eq!(sharded.leaf_count(), 16);
1952        assert_eq!(
1953            sharded.rewind_live_to(15),
1954            Err(TreeError::RewindBeforeCompleted {
1955                minimum: 16,
1956                requested: 15
1957            }),
1958        );
1959    }
1960
1961    #[test]
1962    fn imt_update_rehashes_only_the_selected_path() {
1963        let mut leaves: Vec<Fr> = (1u64..=11).map(Fr::from).collect();
1964        let mut updated = Imt::from_leaves(6, &leaves);
1965        leaves[7] = Fr::from(99u64);
1966        updated.update(7, leaves[7]).unwrap();
1967        assert_eq!(updated.root(), Imt::from_leaves(6, &leaves).root());
1968        assert_eq!(
1969            updated.create_proof(7),
1970            Imt::from_leaves(6, &leaves).create_proof(7)
1971        );
1972    }
1973
1974    #[test]
1975    fn indexed_tree_covers_the_sdk_merkle_surface() {
1976        let leaves: Vec<Fr> = (1u64..=11).map(Fr::from).collect();
1977        let mut tree = IndexedMerkleTree::from_leaves(6, &leaves).unwrap();
1978        assert_eq!(tree.root(), Imt::from_leaves(6, &leaves).root());
1979        assert_eq!(tree.get_index(leaves[7]), Some(7));
1980        assert!(verify_proof(&tree.create_proof(leaves[7]).unwrap()));
1981        assert_eq!(tree.insert(leaves[0]), Err(TreeError::DuplicateLeaf));
1982
1983        tree.insert(Fr::from(12u64)).unwrap();
1984        assert_eq!(tree.leaf_count(), 12);
1985        tree.truncate(10).unwrap();
1986        assert_eq!(tree.leaves(), &leaves[..10]);
1987        assert_eq!(tree.get_index(Fr::from(12u64)), None);
1988    }
1989
1990    #[test]
1991    fn ordered_tree_accepts_duplicate_values_and_proves_by_position() {
1992        let leaves = [Fr::from(7u64), Fr::from(7u64), Fr::from(9u64)];
1993        let tree = OrderedMerkleTree::from_leaves(4, &leaves).unwrap();
1994
1995        assert_eq!(tree.root(), Imt::from_leaves(4, &leaves).root());
1996        assert_eq!(tree.create_proof_at(0).unwrap().leaf, leaves[0]);
1997        assert_eq!(tree.create_proof_at(1).unwrap().leaf, leaves[1]);
1998        assert!(verify_proof(&tree.create_proof_at(1).unwrap()));
1999    }
2000
2001    #[test]
2002    fn notes_frontier_matches_flat_tree_after_every_append() {
2003        let leaves: Vec<Fr> = (1u64..=64).map(Fr::from).collect();
2004        let mut frontier = NotesFrontier::new(6, 3).unwrap();
2005        assert_eq!(frontier.root(), Imt::new(6).root());
2006
2007        for (index, leaf) in leaves.iter().enumerate() {
2008            let appended = frontier.append(*leaf).unwrap();
2009            assert_eq!(appended.leaf_index, index);
2010            assert_eq!(
2011                frontier.root(),
2012                Imt::from_leaves(6, &leaves[..=index]).root(),
2013                "root mismatch after leaf {index}",
2014            );
2015        }
2016        assert_eq!(frontier.leaf_count(), 64);
2017        assert_eq!(
2018            frontier.append(Fr::from(65u64)),
2019            Err(TreeError::TreeFull { depth: 6 })
2020        );
2021    }
2022
2023    #[test]
2024    fn notes_frontier_emits_exact_completed_shard_roots() {
2025        let leaves: Vec<Fr> = (1u64..=21).map(Fr::from).collect();
2026        let mut frontier = NotesFrontier::new(8, 3).unwrap();
2027        let completed = frontier.append_many(&leaves).unwrap();
2028
2029        assert_eq!(completed.len(), 2);
2030        for (shard_index, shard) in completed.iter().enumerate() {
2031            let start = shard_index * 8;
2032            assert_eq!(shard.shard_index, shard_index);
2033            assert_eq!(
2034                shard.root,
2035                Imt::from_leaves(3, &leaves[start..start + 8]).root()
2036            );
2037        }
2038        assert_eq!(frontier.root(), Imt::from_leaves(8, &leaves).root());
2039    }
2040
2041    #[test]
2042    fn notes_frontier_snapshot_is_compact_and_resumes_shard_emission() {
2043        let leaves: Vec<Fr> = (1u64..=37).map(Fr::from).collect();
2044        let mut original = NotesFrontier::new(30, 14).unwrap();
2045        original.append_many(&leaves[..19]).unwrap();
2046
2047        let snapshot = original.encode_snapshot();
2048        assert!(snapshot.len() <= 1_024);
2049        let mut restored = NotesFrontier::from_snapshot_bytes(&snapshot).unwrap();
2050        assert_eq!(restored, original);
2051
2052        let expected = original.append_many(&leaves[19..]).unwrap();
2053        let actual = restored.append_many(&leaves[19..]).unwrap();
2054        assert_eq!(actual, expected);
2055        assert_eq!(restored, original);
2056
2057        let mut corrupt = snapshot;
2058        corrupt.push(0);
2059        assert!(matches!(
2060            NotesFrontier::from_snapshot_bytes(&corrupt),
2061            Err(TreeError::InvalidSnapshot(_))
2062        ));
2063    }
2064
2065    #[test]
2066    fn notes_frontier_full_tree_snapshot_roundtrips() {
2067        let leaves: Vec<Fr> = (1u64..=16).map(Fr::from).collect();
2068        let mut frontier = NotesFrontier::new(4, 2).unwrap();
2069        frontier.append_many(&leaves).unwrap();
2070        assert_eq!(frontier.root(), Imt::from_leaves(4, &leaves).root());
2071
2072        let restored = NotesFrontier::from_snapshot_bytes(&frontier.encode_snapshot()).unwrap();
2073        assert_eq!(restored, frontier);
2074        assert_eq!(restored.root(), Imt::from_leaves(4, &leaves).root());
2075    }
2076
2077    // Soundness: `verify_proof` must REJECT any tampered field - otherwise an
2078    // always-`true` verifier would still pass the round-trip tests above.
2079    #[test]
2080    fn verify_proof_rejects_tampering() {
2081        let leaves: Vec<Fr> = (1u64..=11).map(Fr::from).collect();
2082        let tree = Imt::from_leaves(6, &leaves);
2083        let one = Fr::from(1u64);
2084
2085        for i in 0..leaves.len() {
2086            let good = tree.create_proof(i);
2087            assert!(
2088                verify_proof(&good),
2089                "valid proof must be accepted (leaf {i})"
2090            );
2091
2092            let mut bad_leaf = good.clone();
2093            bad_leaf.leaf += one;
2094            assert!(
2095                !verify_proof(&bad_leaf),
2096                "tampered leaf must be rejected (leaf {i})"
2097            );
2098
2099            let mut bad_sib = good.clone();
2100            bad_sib.siblings[0] += one;
2101            assert!(
2102                !verify_proof(&bad_sib),
2103                "tampered sibling must be rejected (leaf {i})"
2104            );
2105
2106            let mut bad_root = good.clone();
2107            bad_root.root += one;
2108            assert!(
2109                !verify_proof(&bad_root),
2110                "wrong root must be rejected (leaf {i})"
2111            );
2112
2113            // Flipping the index bit selects the sibling on the wrong side; only a
2114            // leaf whose flipped-index neighbour happens to be its mirror could
2115            // collide, which does not occur for this leaf set.
2116            let mut bad_index = good.clone();
2117            bad_index.index ^= 1;
2118            assert!(
2119                !verify_proof(&bad_index),
2120                "wrong index must be rejected (leaf {i})"
2121            );
2122        }
2123    }
2124
2125    #[test]
2126    fn cached_zero_roots_match_freshly_computed() {
2127        // The cache must be a pure memoisation: identical output for every
2128        // depth, including past the cached table where it falls through.
2129        for depth in 0..=MAX_CACHED_ZERO_DEPTH + 2 {
2130            assert_eq!(
2131                zero_roots(depth),
2132                zero_roots_from(depth, Fr::ZERO),
2133                "cached zero roots diverge at depth {depth}",
2134            );
2135        }
2136    }
2137
2138    #[test]
2139    fn cached_zero_roots_are_prefixes_of_one_another() {
2140        let deep = zero_roots(MAX_CACHED_ZERO_DEPTH);
2141        for depth in 0..=MAX_CACHED_ZERO_DEPTH {
2142            assert_eq!(
2143                zero_roots(depth),
2144                deep[..=depth],
2145                "depth {depth} is not a prefix of the full table",
2146            );
2147        }
2148    }
2149
2150    #[test]
2151    fn zero_roots_from_is_unaffected_by_the_cache() {
2152        // A caller-supplied leaf must never be served from the Fr::ZERO table.
2153        let leaf = Fr::from(7u64);
2154        let custom = zero_roots_from(8, leaf);
2155        assert_eq!(custom[0], leaf);
2156        assert_ne!(custom, zero_roots(8));
2157    }
2158
2159    #[test]
2160    fn production_geometry_is_valid() {
2161        let frontier = NotesFrontier::production();
2162        assert_eq!(frontier.depth(), NOTES_TREE_DEPTH);
2163        assert_eq!(frontier.shard_height(), NOTES_SHARD_HEIGHT);
2164        assert_eq!(frontier.shard_size(), NOTES_SHARD_SIZE);
2165        assert_eq!(frontier.leaf_count(), 0);
2166        assert_eq!(frontier.shard_count(), 0);
2167        assert_eq!(
2168            frontier,
2169            NotesFrontier::new(NOTES_TREE_DEPTH, NOTES_SHARD_HEIGHT).unwrap(),
2170        );
2171    }
2172
2173    #[test]
2174    fn shard_count_tracks_completed_shards_only() {
2175        let mut frontier = NotesFrontier::new(6, 3).unwrap();
2176        assert_eq!(frontier.shard_count(), 0);
2177
2178        for index in 0..8u64 {
2179            frontier.append(Fr::from(index + 1)).unwrap();
2180            // A shard of 2^3 leaves only completes on the eighth append.
2181            let expected = usize::from(index == 7);
2182            assert_eq!(
2183                frontier.shard_count(),
2184                expected,
2185                "shard_count wrong after {} leaves",
2186                index + 1,
2187            );
2188        }
2189
2190        frontier.append(Fr::from(9u64)).unwrap();
2191        assert_eq!(frontier.shard_count(), 1, "partial shard must not count");
2192        assert_eq!(frontier.shard_count(), frontier.leaf_count() >> 3);
2193    }
2194
2195    #[test]
2196    fn byte_helpers_match_the_field_api() {
2197        let leaves: Vec<Fr> = (1u64..=9).map(Fr::from).collect();
2198
2199        let mut via_field = NotesFrontier::new(6, 3).unwrap();
2200        let mut via_bytes = NotesFrontier::new(6, 3).unwrap();
2201        for leaf in &leaves {
2202            let expected = via_field.append(*leaf).unwrap();
2203            let actual = via_bytes.append_be_32(&fr_to_be_32(leaf)).unwrap();
2204            assert_eq!(actual, expected);
2205        }
2206
2207        assert_eq!(via_bytes.root_be_32(), fr_to_be_32(&via_field.root()));
2208        assert_eq!(via_bytes, via_field);
2209    }
2210
2211    #[test]
2212    fn append_be_32_rejects_non_canonical_encodings() {
2213        let mut frontier = NotesFrontier::new(6, 3).unwrap();
2214        // 0xff..ff exceeds the BN254 modulus; it must be refused rather than
2215        // silently reduced into the field.
2216        assert_eq!(
2217            frontier.append_be_32(&[0xff; 32]),
2218            Err(TreeError::NonCanonicalField),
2219        );
2220        assert_eq!(frontier.leaf_count(), 0, "rejected leaf must not be stored");
2221    }
2222}