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
14pub 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
28pub 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
34pub 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 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
79pub 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
90pub 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); (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); 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 path.siblings[0] = Fr::rand(&mut rng);
166
167 let (pk, vk) = setup(&mut rng);
171 let (proof, mut pi) = prove(&pk, sk, consumed, path, created, &mut rng);
172 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 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 assert_eq!(svk.ic.len(), 5, "IC length should be 5 for 4 public inputs");
224
225 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 assert_eq!(svk.beta_g2.len(), 384);
235 assert_eq!(sp.b.len(), 384);
236
237 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 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}