Skip to main content

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}