1use 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#[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#[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
132pub const NOTES_TREE_DEPTH: usize = 30;
134
135pub const NOTES_SHARD_HEIGHT: usize = 14;
137
138pub const NOTES_SHARD_SIZE: usize = 1 << NOTES_SHARD_HEIGHT;
140
141pub const NOTES_TREE_VERSION: u32 = 1;
144
145const MAX_CACHED_ZERO_DEPTH: usize = 64;
149
150static ZERO_ROOTS: LazyLock<Vec<Fr>> =
157 LazyLock::new(|| zero_roots_from(MAX_CACHED_ZERO_DEPTH, Fr::ZERO));
158
159pub 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
172pub 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#[derive(Clone)]
185pub struct Imt {
186 depth: usize,
187 zero_leaf: Fr,
188 zeroes: Vec<Fr>, nodes: Vec<Vec<Fr>>, }
191
192impl Imt {
193 pub fn new(depth: usize) -> Self {
195 Self::new_with_zero(depth, Fr::ZERO)
196 }
197
198 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]]; Self {
207 depth,
208 zero_leaf,
209 zeroes: z[..depth].to_vec(),
210 nodes,
211 }
212 }
213
214 pub fn from_leaves(depth: usize, leaves: &[Fr]) -> Self {
216 Self::from_leaves_with_zero(depth, Fr::ZERO, leaves)
217 }
218
219 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 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 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
357pub struct CompletedShard {
358 pub shard_index: usize,
359 pub root: Fr,
360}
361
362#[derive(Clone, Debug, PartialEq, Eq)]
364pub struct FrontierAppend {
365 pub leaf_index: usize,
366 pub completed_shard: Option<CompletedShard>,
367}
368
369#[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 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 pub fn shard_count(&self) -> usize {
439 self.leaf_count >> self.shard_height
440 }
441
442 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 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 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 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 pub fn root_be_32(&self) -> [u8; 32] {
535 fr_to_be_32(&self.root())
536 }
537
538 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#[derive(Clone)]
669pub struct IndexedMerkleTree {
670 tree: Imt,
671 indices: HashMap<Fr, usize>,
672}
673
674#[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
858pub 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#[derive(Clone, Debug, PartialEq, Eq)]
877pub struct OwnedNoteWitness {
878 pub note_id: Fr,
879 pub leaf_index: usize,
880 pub within_shard_siblings: Option<Vec<Fr>>,
882}
883
884#[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 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#[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 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 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 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 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(¬e_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(¬e_id);
1442 self.owned_notes.remove(¬e_id).is_some()
1443 }
1444
1445 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(¬e_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 pub fn witness(&self, note_id: Fr) -> Result<InclusionProof, TreeError> {
1489 let owned = self
1490 .owned_notes
1491 .get(¬e_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 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(¬e_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
1697fn 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
1709fn 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
1738pub 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
1752pub 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 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); 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 #[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 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 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 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 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 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}