imt_tree/tree/mod.rs
1use ff::Field as _;
2use pasta_curves::Fp;
3use rayon::prelude::*;
4
5pub(crate) use crate::hasher::PoseidonHasher;
6
7#[cfg(test)]
8mod tests;
9
10/// Depth of the nullifier Merkle tree.
11///
12/// Each on-chain nullifier produces approximately one gap; with K=2 punctured
13/// ranges, ~n/2 leaves are needed for n nullifiers. The circuit is designed
14/// to support up to 256M nullifiers, so the tree needs ~2^28 leaves:
15/// `log2(256 << 20) + 1 = 29`.
16pub const TREE_DEPTH: usize = 29;
17
18/// A punctured range `[nf_lo, nf_mid, nf_hi]` representing the interval
19/// `(nf_lo, nf_hi) \ {nf_mid}` — two adjacent gaps joined by excluding the
20/// nullifier between them.
21///
22/// With K=2, each leaf stores three sorted nullifier boundaries. The leaf
23/// commitment is `Poseidon3(nf_lo, nf_mid, nf_hi)`.
24pub type PuncturedRange = [Fp; 3];
25
26/// Build punctured ranges (K=2) from a sorted, deduplicated nullifier list.
27///
28/// Groups consecutive nullifiers into overlapping triples:
29/// `[nf_0, nf_1, nf_2]`, `[nf_2, nf_3, nf_4]`, `[nf_4, nf_5, nf_6]`, ...
30///
31/// Each triple covers the punctured interval `(nf_lo, nf_hi) \ {nf_mid}`.
32/// Consecutive triples share boundary nullifiers, so every gap between
33/// adjacent nullifiers is covered by exactly one leaf.
34///
35/// # Panics
36///
37/// Panics if `sorted_nfs` has fewer than 3 elements or an even length
38/// (which would leave a trailing gap without a matching triple — callers
39/// should ensure an odd count via sentinel injection).
40pub fn build_punctured_ranges(sorted_nfs: &[Fp]) -> Vec<PuncturedRange> {
41 let n = sorted_nfs.len();
42 assert!(
43 n >= 3,
44 "need at least 3 sorted nullifiers for K=2 punctured ranges, got {n}"
45 );
46 assert!(
47 n % 2 == 1,
48 "sorted nullifier count must be odd for K=2 (got {n}); \
49 inject an additional sentinel to fix"
50 );
51
52 let num_leaves = (n - 1) / 2;
53 (0..num_leaves)
54 .map(|i| {
55 let base = i * 2;
56 let (lo, mid, hi) = (sorted_nfs[base], sorted_nfs[base + 1], sorted_nfs[base + 2]);
57 assert!(
58 lo < mid && mid < hi,
59 "punctured range {i} violates strict ordering: \
60 nf_lo={lo:?}, nf_mid={mid:?}, nf_hi={hi:?} \
61 (input must be sorted and deduplicated)"
62 );
63 [lo, mid, hi]
64 })
65 .collect()
66}
67
68/// Hash each punctured range triple into a single leaf commitment.
69pub fn commit_punctured_ranges(ranges: &[PuncturedRange]) -> Vec<Fp> {
70 ranges
71 .par_iter()
72 .map_init(PoseidonHasher::new, |hasher, &[a, b, c]| {
73 hasher.hash3(a, b, c)
74 })
75 .collect()
76}
77
78/// Find the punctured-range index that contains `value`.
79///
80/// Returns `Some(i)` where `ranges[i]` is `[nf_lo, nf_mid, nf_hi]` and
81/// `nf_lo < value < nf_hi` and `value != nf_mid`. Returns `None` if the
82/// value is an existing nullifier.
83pub fn find_punctured_range_for_value(ranges: &[PuncturedRange], value: Fp) -> Option<usize> {
84 let i = ranges.partition_point(|[nf_lo, _, _]| *nf_lo < value);
85 if i == 0 {
86 return None;
87 }
88 let idx = i - 1;
89 let [nf_lo, nf_mid, nf_hi] = ranges[idx];
90 let offset = value - nf_lo;
91 let span = nf_hi - nf_lo;
92 if offset == Fp::zero() || offset >= span {
93 return None;
94 }
95 if value == nf_mid {
96 return None;
97 }
98 Some(idx)
99}
100
101/// Verify that every punctured range has outer span `≤ 2^250`.
102///
103/// For K=2, the outer span `nf_hi - nf_lo` covers two consecutive sentinel
104/// intervals. With sentinel spacing `2^249`, the maximum span is
105/// `2 * 2^249 = 2^250`, which matches the circuit's 250-bit range check
106/// (25 limbs × 10 bits).
107pub fn verify_punctured_range_spans(ranges: &[PuncturedRange]) -> anyhow::Result<()> {
108 let max_span = Fp::from(2u64).pow([250, 0, 0, 0]);
109 for (i, &[nf_lo, _, nf_hi]) in ranges.iter().enumerate() {
110 let span = nf_hi - nf_lo;
111 anyhow::ensure!(
112 span <= max_span,
113 "punctured range {i} has span > 2^250: nf_lo={nf_lo:?}, nf_hi={nf_hi:?}"
114 );
115 }
116 Ok(())
117}
118
119/// Pre-compute the empty subtree hash at each tree level.
120///
121/// `empty[0] = hash3(0, 0, 0)` -- the commitment of an all-zero punctured range.
122/// `empty[i] = hash(empty[i-1], empty[i-1])` for higher levels.
123pub fn precompute_empty_hashes() -> [Fp; TREE_DEPTH] {
124 let hasher = PoseidonHasher::new();
125 let mut empty = [Fp::default(); TREE_DEPTH];
126 empty[0] = hasher.hash3(Fp::zero(), Fp::zero(), Fp::zero());
127 for i in 1..TREE_DEPTH {
128 empty[i] = hasher.hash(empty[i - 1], empty[i - 1]);
129 }
130 empty
131}
132
133/// Build Merkle tree levels bottom-up from leaf hashes.
134///
135/// `depth` controls the number of tree levels (use `TREE_DEPTH` for a full
136/// depth-29 tree, or a smaller value like 25 for the PIR tree).
137/// Returns `(root, levels)` where `levels[0]` contains leaf hashes and
138/// `levels[depth-1]` contains the root's two children.
139///
140/// Each level is padded to even length using the pre-computed empty hash so
141/// that pair-wise hashing produces the next level cleanly. All intermediate
142/// layers are retained so Merkle auth paths can be extracted in O(`depth`)
143/// via simple sibling lookups.
144pub fn build_levels(
145 mut leaves: Vec<Fp>,
146 empty: &[Fp; TREE_DEPTH],
147 depth: usize,
148) -> (Fp, Vec<Vec<Fp>>) {
149 let hasher = PoseidonHasher::new();
150 let mut levels: Vec<Vec<Fp>> = Vec::with_capacity(depth);
151
152 // Level 0 = leaf commitments, padded to even length.
153 // Takes ownership of `leaves` to avoid a 1.6 GB memcpy at scale.
154 if leaves.is_empty() {
155 leaves.push(empty[0]);
156 }
157 if leaves.len() & 1 == 1 {
158 leaves.push(empty[0]);
159 }
160 levels.push(leaves);
161
162 const PAR_THRESHOLD: usize = 1024;
163
164 for i in 0..depth - 1 {
165 let prev = &levels[i];
166 let pairs = prev.len() / 2;
167 let mut next: Vec<Fp> = if pairs >= PAR_THRESHOLD {
168 prev.par_chunks_exact(2)
169 .map_init(PoseidonHasher::new, |h, pair| h.hash(pair[0], pair[1]))
170 .collect()
171 } else {
172 (0..pairs)
173 .map(|j| hasher.hash(prev[j * 2], prev[j * 2 + 1]))
174 .collect()
175 };
176 if next.len() & 1 == 1 {
177 next.push(empty[i + 1]);
178 }
179 levels.push(next);
180 }
181
182 let top = &levels[depth - 1];
183 let root = hasher.hash(top[0], top[1]);
184
185 (root, levels)
186}