Skip to main content

r14_circuit/
lib.rs

1pub mod merkle_gadget;
2pub mod poseidon_gadget;
3pub mod transfer;
4
5use ark_bls12_381::{Bls12_381, Fr};
6use ark_groth16::{Groth16, PreparedVerifyingKey, ProvingKey, VerifyingKey};
7use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem};
8use ark_snark::SNARK;
9use ark_std::rand::{CryptoRng, RngCore};
10use r14_types::{MerklePath, Note};
11
12pub use transfer::TransferCircuit;
13
14/// Public inputs for a transfer proof
15pub struct PublicInputs {
16    pub old_root: Fr,
17    pub nullifier: Fr,
18    pub out_commitment_0: Fr,
19    pub out_commitment_1: Fr,
20}
21
22impl PublicInputs {
23    pub fn to_vec(&self) -> Vec<Fr> {
24        vec![self.old_root, self.nullifier, self.out_commitment_0, self.out_commitment_1]
25    }
26}
27
28/// Run Groth16 trusted setup for the transfer circuit
29pub fn setup<R: RngCore + CryptoRng>(rng: &mut R) -> (ProvingKey<Bls12_381>, VerifyingKey<Bls12_381>) {
30    let circuit = TransferCircuit::empty();
31    Groth16::<Bls12_381>::circuit_specific_setup(circuit, rng).expect("setup failed")
32}
33
34/// Generate a Groth16 proof for a private transfer
35pub fn prove<R: RngCore + CryptoRng>(
36    pk: &ProvingKey<Bls12_381>,
37    secret_key: Fr,
38    consumed_note: Note,
39    merkle_path: MerklePath,
40    created_notes: [Note; 2],
41    rng: &mut R,
42) -> (ark_groth16::Proof<Bls12_381>, PublicInputs) {
43    // Compute public inputs natively
44    let cm = r14_poseidon::commitment(&consumed_note);
45
46    let mut current = cm;
47    for i in 0..merkle_path.siblings.len() {
48        if merkle_path.indices[i] {
49            current = r14_poseidon::hash2(merkle_path.siblings[i], current);
50        } else {
51            current = r14_poseidon::hash2(current, merkle_path.siblings[i]);
52        }
53    }
54    let old_root = current;
55
56    let nullifier = r14_poseidon::poseidon_hash(&[secret_key, consumed_note.nonce]);
57    let out_cm_0 = r14_poseidon::commitment(&created_notes[0]);
58    let out_cm_1 = r14_poseidon::commitment(&created_notes[1]);
59
60    let circuit = TransferCircuit {
61        secret_key: Some(secret_key),
62        consumed_note: Some(consumed_note),
63        merkle_path: Some(merkle_path),
64        created_notes: Some(created_notes),
65    };
66
67    let proof = Groth16::<Bls12_381>::prove(pk, circuit, rng).expect("proving failed");
68
69    let public_inputs = PublicInputs {
70        old_root,
71        nullifier,
72        out_commitment_0: out_cm_0,
73        out_commitment_1: out_cm_1,
74    };
75
76    (proof, public_inputs)
77}
78
79/// Verify a proof off-chain
80pub fn verify_offchain(
81    vk: &VerifyingKey<Bls12_381>,
82    proof: &ark_groth16::Proof<Bls12_381>,
83    public_inputs: &PublicInputs,
84) -> bool {
85    let pvk = PreparedVerifyingKey::from(vk.clone());
86    Groth16::<Bls12_381>::verify_with_processed_vk(&pvk, &public_inputs.to_vec(), proof)
87        .unwrap_or(false)
88}
89
90/// Count constraints in the transfer circuit
91pub fn constraint_count() -> usize {
92    let cs = ConstraintSystem::<Fr>::new_ref();
93    cs.set_optimization_goal(ark_relations::r1cs::OptimizationGoal::Constraints);
94    cs.set_mode(ark_relations::r1cs::SynthesisMode::Setup);
95    let circuit = TransferCircuit::empty();
96    circuit.generate_constraints(cs.clone()).expect("constraint generation failed");
97    cs.num_constraints()
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use ark_ff::UniformRand;
104    use ark_relations::r1cs::ConstraintSynthesizer;
105    use ark_std::rand::{rngs::StdRng, SeedableRng};
106    use r14_types::{MerklePath, Note, SecretKey, MERKLE_DEPTH};
107
108    fn test_rng() -> StdRng {
109        StdRng::seed_from_u64(42)
110    }
111
112    fn build_dummy_merkle_path(rng: &mut impl RngCore) -> MerklePath {
113        let siblings: Vec<Fr> = (0..MERKLE_DEPTH).map(|_| Fr::rand(rng)).collect();
114        let indices: Vec<bool> = (0..MERKLE_DEPTH).map(|i| i % 2 == 0).collect();
115        MerklePath { siblings, indices }
116    }
117
118    fn test_scenario(rng: &mut impl RngCore) -> (Fr, Note, MerklePath, [Note; 2]) {
119        let sk = SecretKey::random(rng);
120        let owner = r14_poseidon::owner_hash(&sk);
121        let consumed = Note::new(1000, 1, owner.0, rng);
122        let path = build_dummy_merkle_path(rng);
123
124        let recipient_sk = SecretKey::random(rng);
125        let recipient_owner = r14_poseidon::owner_hash(&recipient_sk);
126        let note_0 = Note::new(700, 1, recipient_owner.0, rng);
127        let note_1 = Note::new(300, 1, owner.0, rng); // change back to sender
128
129        (sk.0, consumed, path, [note_0, note_1])
130    }
131
132    #[test]
133    fn test_valid_transfer() {
134        let mut rng = test_rng();
135        let (sk, consumed, path, created) = test_scenario(&mut rng);
136
137        let (pk, vk) = setup(&mut rng);
138        let (proof, pi) = prove(&pk, sk, consumed, path, created, &mut rng);
139        assert!(verify_offchain(&vk, &proof, &pi));
140    }
141
142    #[test]
143    fn test_wrong_secret_key() {
144        let mut rng = test_rng();
145        let (_, consumed, path, created) = test_scenario(&mut rng);
146        let wrong_sk = Fr::rand(&mut rng); // wrong key
147
148        let circuit = TransferCircuit {
149            secret_key: Some(wrong_sk),
150            consumed_note: Some(consumed),
151            merkle_path: Some(path),
152            created_notes: Some(created),
153        };
154
155        let cs = ConstraintSystem::<Fr>::new_ref();
156        circuit.generate_constraints(cs.clone()).unwrap();
157        assert!(!cs.is_satisfied().unwrap(), "should fail: wrong secret key");
158    }
159
160    #[test]
161    fn test_wrong_merkle_path() {
162        let mut rng = test_rng();
163        let (sk, consumed, mut path, created) = test_scenario(&mut rng);
164        // Corrupt one sibling
165        path.siblings[0] = Fr::rand(&mut rng);
166
167        // The circuit will compute a different root than what gets set as public input
168        // We need to test at the proof level — the circuit itself always computes consistently
169        // So instead: use prove() which computes root from the bad path, then tamper the root
170        let (pk, vk) = setup(&mut rng);
171        let (proof, mut pi) = prove(&pk, sk, consumed, path, created, &mut rng);
172        // Tamper with root to simulate inclusion failure
173        pi.old_root = Fr::rand(&mut rng);
174        assert!(!verify_offchain(&vk, &proof, &pi), "should fail: wrong root");
175    }
176
177    #[test]
178    fn test_value_mismatch() {
179        let mut rng = test_rng();
180        let sk = SecretKey::random(&mut rng);
181        let owner = r14_poseidon::owner_hash(&sk);
182        let consumed = Note::new(1000, 1, owner.0, &mut rng);
183        let path = build_dummy_merkle_path(&mut rng);
184
185        let recipient_sk = SecretKey::random(&mut rng);
186        let recipient_owner = r14_poseidon::owner_hash(&recipient_sk);
187        // Values don't sum to 1000
188        let note_0 = Note::new(600, 1, recipient_owner.0, &mut rng);
189        let note_1 = Note::new(300, 1, owner.0, &mut rng);
190
191        let circuit = TransferCircuit {
192            secret_key: Some(sk.0),
193            consumed_note: Some(consumed),
194            merkle_path: Some(path),
195            created_notes: Some([note_0, note_1]),
196        };
197
198        let cs = ConstraintSystem::<Fr>::new_ref();
199        circuit.generate_constraints(cs.clone()).unwrap();
200        assert!(!cs.is_satisfied().unwrap(), "should fail: value mismatch");
201    }
202
203    #[test]
204    fn test_constraint_count() {
205        let count = constraint_count();
206        println!("Transfer circuit constraint count: {}", count);
207        assert!(count < 20_000, "constraint count {} exceeds 20K limit", count);
208        assert!(count > 1_000, "constraint count {} suspiciously low", count);
209    }
210
211    #[test]
212    fn test_serialization_roundtrip() {
213        let mut rng = test_rng();
214        let (sk, consumed, path, created) = test_scenario(&mut rng);
215
216        let (pk, vk) = setup(&mut rng);
217        let (proof, pi) = prove(&pk, sk, consumed, path, created, &mut rng);
218
219        let svk = r14_sdk::serialize::serialize_vk_for_soroban(&vk);
220        let (sp, spi) = r14_sdk::serialize::serialize_proof_for_soroban(&proof, &pi.to_vec());
221
222        // IC length = 5 (1 constant + 4 public inputs)
223        assert_eq!(svk.ic.len(), 5, "IC length should be 5 for 4 public inputs");
224
225        // G1 = 96 bytes = 192 hex chars
226        assert_eq!(svk.alpha_g1.len(), 192);
227        assert_eq!(sp.a.len(), 192);
228        assert_eq!(sp.c.len(), 192);
229        for ic in &svk.ic {
230            assert_eq!(ic.len(), 192);
231        }
232
233        // G2 = 192 bytes = 384 hex chars
234        assert_eq!(svk.beta_g2.len(), 384);
235        assert_eq!(sp.b.len(), 384);
236
237        // Fr = 32 bytes = 64 hex chars
238        assert_eq!(spi.len(), 4);
239        for pi_hex in &spi {
240            assert_eq!(pi_hex.len(), 64);
241        }
242    }
243
244    #[test]
245    fn test_app_tag_mismatch() {
246        let mut rng = test_rng();
247        let sk = SecretKey::random(&mut rng);
248        let owner = r14_poseidon::owner_hash(&sk);
249        let consumed = Note::new(1000, 1, owner.0, &mut rng);
250        let path = build_dummy_merkle_path(&mut rng);
251
252        let recipient_sk = SecretKey::random(&mut rng);
253        let recipient_owner = r14_poseidon::owner_hash(&recipient_sk);
254        // app_tag mismatch: consumed=1, created=2
255        let note_0 = Note::new(700, 2, recipient_owner.0, &mut rng);
256        let note_1 = Note::new(300, 1, owner.0, &mut rng);
257
258        let circuit = TransferCircuit {
259            secret_key: Some(sk.0),
260            consumed_note: Some(consumed),
261            merkle_path: Some(path),
262            created_notes: Some([note_0, note_1]),
263        };
264
265        let cs = ConstraintSystem::<Fr>::new_ref();
266        circuit.generate_constraints(cs.clone()).unwrap();
267        assert!(!cs.is_satisfied().unwrap(), "should fail: app tag mismatch");
268    }
269}