Skip to main content

imt_tree/
proof.rs

1use pasta_curves::Fp;
2
3use crate::hasher::PoseidonHasher;
4use crate::tree::TREE_DEPTH;
5
6/// Number of excluded nullifiers per punctured-range leaf (K=2).
7///
8/// Each leaf stores K+1 = 3 boundary nullifiers `[nf_lo, nf_mid, nf_hi]` and
9/// covers the punctured interval `(nf_lo, nf_hi) \ {nf_mid}`.
10pub const PUNCTURE_K: usize = 2;
11
12/// Walk a Merkle authentication path and check it recomputes to `root`.
13fn verify_merkle_path(
14    hasher: &PoseidonHasher,
15    leaf: Fp,
16    mut pos: u32,
17    path: &[Fp; TREE_DEPTH],
18    root: Fp,
19) -> bool {
20    let mut current = leaf;
21    for sibling in path.iter() {
22        let (l, r) = if pos & 1 == 0 {
23            (current, *sibling)
24        } else {
25            (*sibling, current)
26        };
27        current = hasher.hash(l, r);
28        pos >>= 1;
29    }
30    current == root
31}
32
33/// Circuit-compatible IMT non-membership proof for punctured-range leaves (K=2).
34///
35/// Instead of storing a single `(low, width)` gap, each leaf commits to three
36/// sorted nullifier boundaries `[nf_lo, nf_mid, nf_hi]` via `Poseidon3(...)`.
37/// The leaf covers the punctured interval `(nf_lo, nf_hi) \ {nf_mid}`.
38///
39/// ## Circuit witnesses
40///
41/// Each field maps directly to a witness in the delegation circuit's
42/// condition 13 (IMT non-membership):
43///
44/// - `root`: public input, constrained equal to `nf_imt_root` in the
45///   `q_per_note` gate.
46/// - `nf_bounds`: three field elements `[nf_lo, nf_mid, nf_hi]`, witnessed
47///   and hashed in-circuit via `Poseidon3` (two permutations) to produce
48///   the leaf commitment.
49/// - `leaf_pos`: position bits that determine swap ordering at each Merkle
50///   level via 29 `q_imt_swap` gates.
51/// - `path`: 29-level authentication path (25 PIR siblings + 4 empty-hash
52///   padding for circuit depth).
53///
54/// ## Circuit constraints (condition 13)
55///
56/// 1. **Leaf hash**: `leaf = Poseidon3(nf_lo, nf_mid, nf_hi)` — two
57///    Poseidon permutations (width-3 sponge, `ConstantLength<3>`).
58/// 2. **Merkle path**: 29 × swap + Poseidon → `computed_root`.
59/// 3. **Strict interval**: `nf_lo < value < nf_hi` via two 250-bit range
60///    checks on `value - nf_lo - 1` and `nf_hi - value - 1`.
61/// 4. **Non-equality**: `value ≠ nf_mid` by witnessing `inverse(value - nf_mid)`.
62/// 5. **Root pin**: `computed_root = nf_imt_root` (not gated on `is_note_real`).
63///
64/// ## Soundness invariants
65///
66/// The interval check relies on `nf_hi - nf_lo ≤ 2^250`, which is
67/// guaranteed by sentinel nullifiers at `k × 2^249` spacing (each K=2
68/// leaf spans two intervals: `2 × 2^249 = 2^250`). Without this bound,
69/// a field-wrapping span would defeat the range check.
70///
71/// The ordering `nf_lo < nf_mid < nf_hi` is enforced by tree construction
72/// (sorted nullifier input) and locked by the Merkle commitment — a
73/// forger cannot alter `nf_bounds` without breaking the Merkle path.
74#[derive(Clone, Debug)]
75pub struct ImtProofData {
76    /// The Merkle root of the IMT.
77    pub root: Fp,
78    /// Three sorted nullifier boundaries: `[nf_lo, nf_mid, nf_hi]`.
79    pub nf_bounds: [Fp; PUNCTURE_K + 1],
80    /// Position of the leaf in the tree.
81    pub leaf_pos: u32,
82    /// Sibling hashes along the 29-level Merkle authentication path.
83    pub path: [Fp; TREE_DEPTH],
84}
85
86impl ImtProofData {
87    /// Verify this proof out-of-circuit.
88    ///
89    /// Checks that `value` lies strictly inside the punctured interval
90    /// `(nf_lo, nf_hi) \ {nf_mid}` and that the Merkle path recomputes
91    /// to `root`.
92    pub fn verify(&self, value: Fp) -> bool {
93        let [nf_lo, nf_mid, nf_hi] = self.nf_bounds;
94
95        // value must be strictly between the outer boundaries.
96        // offset = value - nf_lo must satisfy 0 < offset < span = nf_hi - nf_lo.
97        // If value <= nf_lo the subtraction wraps to a huge value (> span).
98        let offset = value - nf_lo;
99        let span = nf_hi - nf_lo;
100        if offset == Fp::zero() || offset >= span {
101            return false;
102        }
103
104        // value must not equal the interior nullifier.
105        if value == nf_mid {
106            return false;
107        }
108
109        let hasher = PoseidonHasher::new();
110        let leaf = hasher.hash3(nf_lo, nf_mid, nf_hi);
111        verify_merkle_path(&hasher, leaf, self.leaf_pos, &self.path, self.root)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::test_helpers::fp;
119    use crate::tree::{precompute_empty_hashes, TREE_DEPTH};
120
121    fn make_punctured_proof(nf_bounds: [Fp; 3]) -> ImtProofData {
122        let hasher = PoseidonHasher::new();
123        let empty = precompute_empty_hashes();
124        let leaf = hasher.hash3(nf_bounds[0], nf_bounds[1], nf_bounds[2]);
125        let mut current = leaf;
126        let mut path = [Fp::zero(); TREE_DEPTH];
127        for i in 0..TREE_DEPTH {
128            path[i] = empty[i];
129            current = hasher.hash(current, empty[i]);
130        }
131        ImtProofData {
132            root: current,
133            nf_bounds,
134            leaf_pos: 0,
135            path,
136        }
137    }
138
139    #[test]
140    fn punctured_verify_accepts_value_in_gap() {
141        let proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
142        assert!(proof.verify(fp(15)), "value in first gap should pass");
143        assert!(proof.verify(fp(25)), "value in second gap should pass");
144        assert!(proof.verify(fp(11)), "value just above nf_lo should pass");
145        assert!(proof.verify(fp(29)), "value just below nf_hi should pass");
146    }
147
148    #[test]
149    fn punctured_verify_rejects_boundary_nullifiers() {
150        let proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
151        assert!(!proof.verify(fp(10)), "nf_lo should be rejected");
152        assert!(!proof.verify(fp(20)), "nf_mid should be rejected");
153        assert!(!proof.verify(fp(30)), "nf_hi should be rejected");
154    }
155
156    #[test]
157    fn punctured_verify_rejects_out_of_range() {
158        let proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
159        assert!(!proof.verify(fp(5)), "value below nf_lo should fail");
160        assert!(!proof.verify(fp(31)), "value above nf_hi should fail");
161        assert!(!proof.verify(fp(0)), "zero should fail");
162        assert!(!proof.verify(Fp::one().neg()), "p-1 should fail");
163    }
164
165    #[test]
166    fn punctured_verify_rejects_tampered_path() {
167        let mut proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
168        proof.path[0] += Fp::one();
169        assert!(!proof.verify(fp(15)), "tampered path should fail");
170    }
171
172    #[test]
173    fn punctured_verify_rejects_tampered_bounds() {
174        let mut proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
175        proof.nf_bounds[1] = fp(21);
176        assert!(!proof.verify(fp(15)), "tampered nf_mid should fail Merkle check");
177    }
178
179    #[test]
180    fn punctured_verify_rejects_wrong_root() {
181        let mut proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
182        proof.root = Fp::zero();
183        assert!(!proof.verify(fp(15)), "wrong root should fail");
184    }
185
186    #[test]
187    fn punctured_verify_rejects_wrong_position() {
188        let mut proof = make_punctured_proof([fp(10), fp(20), fp(30)]);
189        proof.leaf_pos = 1;
190        assert!(!proof.verify(fp(15)), "wrong leaf_pos should fail");
191    }
192}