sonobe/folding-schemes/src/frontend/circom/mod.rs
folding-schemes/src/frontend/circom/mod.rsRaw
1use crate::frontend::FCircuit;
2use crate::frontend::FpVar::Var;
3use crate::utils::PathOrBin;
4use crate::Error;
5use ark_circom::circom::{CircomCircuit, R1CS as CircomR1CS};
6use ark_ff::PrimeField;
7use ark_r1cs_std::alloc::AllocVar;
8use ark_r1cs_std::fields::fp::FpVar;
9use ark_r1cs_std::R1CSVar;
10use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
11use ark_std::fmt::Debug;
12use num_bigint::BigInt;
13use std::rc::Rc;
14use std::{fmt, usize};
15
16pub mod utils;
17use utils::CircomWrapper;
18
19type ClosurePointer<F> = Rc<dyn Fn(usize, Vec<F>, Vec<F>) -> Result<Vec<F>, Error>>;
20
21#[derive(Clone)]
22struct CustomStepNative<F: PrimeField> {
23 func: ClosurePointer<F>,
24}
25
26impl<F: PrimeField> fmt::Debug for CustomStepNative<F> {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(
29 f,
30 "Function pointer: {:?}",
31 std::any::type_name::<fn(usize, Vec<F>, Vec<F>) -> Result<Vec<F>, Error>>()
32 )
33 }
34}
35
36/// Define CircomFCircuit
37#[derive(Clone, Debug)]
38pub struct CircomFCircuit<F: PrimeField> {
39 circom_wrapper: CircomWrapper<F>,
40 pub state_len: usize,
41 pub external_inputs_len: usize,
42 r1cs: CircomR1CS<F>,
43 custom_step_native_code: Option<CustomStepNative<F>>,
44}
45
46impl<F: PrimeField> CircomFCircuit<F> {
47 pub fn set_custom_step_native(&mut self, func: ClosurePointer<F>) {
48 self.custom_step_native_code = Some(CustomStepNative::<F> { func });
49 }
50
51 pub fn execute_custom_step_native(
52 &self,
53 _i: usize,
54 z_i: Vec<F>,
55 external_inputs: Vec<F>,
56 ) -> Result<Vec<F>, Error> {
57 if let Some(code) = &self.custom_step_native_code {
58 (code.func)(_i, z_i, external_inputs)
59 } else {
60 #[cfg(test)]
61 assert_eq!(z_i.len(), self.state_len());
62 #[cfg(test)]
63 assert_eq!(external_inputs.len(), self.external_inputs_len());
64
65 let inputs_bi = z_i
66 .iter()
67 .map(|val| self.circom_wrapper.ark_primefield_to_num_bigint(*val))
68 .collect::<Vec<BigInt>>();
69 let mut inputs_map = vec![("ivc_input".to_string(), inputs_bi)];
70
71 if self.external_inputs_len() > 0 {
72 let external_inputs_bi = external_inputs
73 .iter()
74 .map(|val| self.circom_wrapper.ark_primefield_to_num_bigint(*val))
75 .collect::<Vec<BigInt>>();
76 inputs_map.push(("external_inputs".to_string(), external_inputs_bi));
77 }
78
79 // Computes witness
80 let witness = self
81 .circom_wrapper
82 .extract_witness(&inputs_map)
83 .map_err(|e| {
84 Error::WitnessCalculationError(format!("Failed to calculate witness: {}", e))
85 })?;
86
87 // Extracts the z_i1(next state) from the witness vector.
88 let z_i1 = witness[1..1 + self.state_len()].to_vec();
89 Ok(z_i1)
90 }
91 }
92}
93
94impl<F: PrimeField> FCircuit<F> for CircomFCircuit<F> {
95 /// (r1cs_path, wasm_path, state_len, external_inputs_len)
96 type Params = (PathOrBin, PathOrBin, usize, usize);
97
98 fn new(params: Self::Params) -> Result<Self, Error> {
99 let (r1cs_path, wasm_path, state_len, external_inputs_len) = params;
100 let circom_wrapper = CircomWrapper::new(r1cs_path, wasm_path)?;
101
102 let r1cs = circom_wrapper.extract_r1cs()?;
103 Ok(Self {
104 circom_wrapper,
105 state_len,
106 external_inputs_len,
107 r1cs,
108 custom_step_native_code: None,
109 })
110 }
111
112 fn state_len(&self) -> usize {
113 self.state_len
114 }
115 fn external_inputs_len(&self) -> usize {
116 self.external_inputs_len
117 }
118
119 fn step_native(
120 &self,
121 _i: usize,
122 z_i: Vec<F>,
123 external_inputs: Vec<F>,
124 ) -> Result<Vec<F>, Error> {
125 self.execute_custom_step_native(_i, z_i, external_inputs)
126 }
127
128 fn generate_step_constraints(
129 &self,
130 cs: ConstraintSystemRef<F>,
131 _i: usize,
132 z_i: Vec<FpVar<F>>,
133 external_inputs: Vec<FpVar<F>>,
134 ) -> Result<Vec<FpVar<F>>, SynthesisError> {
135 #[cfg(test)]
136 assert_eq!(z_i.len(), self.state_len());
137 #[cfg(test)]
138 assert_eq!(external_inputs.len(), self.external_inputs_len());
139
140 let input_values = self.fpvars_to_bigints(&z_i)?;
141 let mut inputs_map = vec![("ivc_input".to_string(), input_values)];
142
143 if self.external_inputs_len() > 0 {
144 let external_inputs_bi = self.fpvars_to_bigints(&external_inputs)?;
145 inputs_map.push(("external_inputs".to_string(), external_inputs_bi));
146 }
147
148 let witness = self
149 .circom_wrapper
150 .extract_witness(&inputs_map)
151 .map_err(|_| SynthesisError::AssignmentMissing)?;
152
153 // Since public inputs are already allocated variables, we will tell `circom-compat` to not re-allocate those
154 let mut already_allocated_public_inputs = vec![];
155 for var in z_i.iter() {
156 match var {
157 Var(var) => already_allocated_public_inputs.push(var.variable),
158 _ => return Err(SynthesisError::Unsatisfiable), // allocated z_i should be Var
159 }
160 }
161
162 // Initializes the CircomCircuit.
163 let circom_circuit = CircomCircuit {
164 r1cs: self.r1cs.clone(),
165 witness: Some(witness.clone()),
166 public_inputs_indexes: already_allocated_public_inputs,
167 allocate_inputs_as_witnesses: true,
168 };
169
170 // Generates the constraints for the circom_circuit.
171 circom_circuit.generate_constraints(cs.clone())?;
172
173 // TODO: https://github.com/privacy-scaling-explorations/sonobe/issues/104
174 // We disable checking constraints for now
175 // Checks for constraint satisfaction.
176 // if !cs.is_satisfied().unwrap() {
177 // return Err(SynthesisError::Unsatisfiable);
178 // }
179
180 // Extracts the z_i1(next state) from the witness vector.
181 let z_i1: Vec<FpVar<F>> = Vec::<FpVar<F>>::new_witness(cs.clone(), || {
182 Ok(witness[1..1 + self.state_len()].to_vec())
183 })?;
184
185 Ok(z_i1)
186 }
187}
188
189impl<F: PrimeField> CircomFCircuit<F> {
190 fn fpvars_to_bigints(&self, fpVars: &[FpVar<F>]) -> Result<Vec<BigInt>, SynthesisError> {
191 let mut input_values = Vec::new();
192 // converts each FpVar to PrimeField value, then to num_bigint::BigInt.
193 for fp_var in fpVars.iter() {
194 // extracts the PrimeField value from FpVar.
195 let primefield_value = fp_var.value()?;
196 // converts the PrimeField value to num_bigint::BigInt.
197 let num_bigint_value = self
198 .circom_wrapper
199 .ark_primefield_to_num_bigint(primefield_value);
200 input_values.push(num_bigint_value);
201 }
202 Ok(input_values)
203 }
204}
205
206#[cfg(test)]
207pub mod tests {
208 use super::*;
209 use ark_bn254::Fr;
210 use ark_relations::r1cs::ConstraintSystem;
211 use std::path::PathBuf;
212
213 // Tests the step_native function of CircomFCircuit.
214 #[test]
215 fn test_circom_step_native() {
216 let r1cs_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit.r1cs");
217 let wasm_path =
218 PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
219
220 let circom_fcircuit =
221 CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
222
223 let z_i = vec![Fr::from(3u32)];
224 let z_i1 = circom_fcircuit.step_native(1, z_i, vec![]).unwrap();
225 assert_eq!(z_i1, vec![Fr::from(35u32)]);
226 }
227
228 // Tests the generate_step_constraints function of CircomFCircuit.
229 #[test]
230 fn test_circom_step_constraints() {
231 let r1cs_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit.r1cs");
232 let wasm_path =
233 PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
234
235 let circom_fcircuit =
236 CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
237
238 let cs = ConstraintSystem::<Fr>::new_ref();
239
240 let z_i = vec![Fr::from(3u32)];
241
242 let z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap();
243 let z_i1_var = circom_fcircuit
244 .generate_step_constraints(cs.clone(), 1, z_i_var, vec![])
245 .unwrap();
246 assert_eq!(z_i1_var.value().unwrap(), vec![Fr::from(35u32)]);
247 }
248
249 // Tests the WrapperCircuit with CircomFCircuit.
250 #[test]
251 fn test_wrapper_circomtofcircuit() {
252 let r1cs_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit.r1cs");
253 let wasm_path =
254 PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
255
256 let circom_fcircuit =
257 CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
258
259 // Allocates z_i1 by using step_native function.
260 let z_i = vec![Fr::from(3_u32)];
261 let wrapper_circuit = crate::frontend::utils::WrapperCircuit {
262 FC: circom_fcircuit.clone(),
263 z_i: Some(z_i.clone()),
264 z_i1: Some(circom_fcircuit.step_native(0, z_i.clone(), vec![]).unwrap()),
265 };
266
267 let cs = ConstraintSystem::<Fr>::new_ref();
268
269 wrapper_circuit.generate_constraints(cs.clone()).unwrap();
270 assert!(
271 cs.is_satisfied().unwrap(),
272 "Constraint system is not satisfied"
273 );
274 }
275
276 #[test]
277 fn test_circom_external_inputs() {
278 let r1cs_path =
279 PathBuf::from("./src/frontend/circom/test_folder/with_external_inputs.r1cs");
280 let wasm_path = PathBuf::from(
281 "./src/frontend/circom/test_folder/with_external_inputs_js/with_external_inputs.wasm",
282 );
283 let circom_fcircuit =
284 CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 2)).unwrap(); // state_len:1, external_inputs_len:2
285 let cs = ConstraintSystem::<Fr>::new_ref();
286 let z_i = vec![Fr::from(3u32)];
287 let external_inputs = vec![Fr::from(6u32), Fr::from(7u32)];
288
289 // run native step
290 let z_i1_native = circom_fcircuit
291 .step_native(1, z_i.clone(), external_inputs.clone())
292 .unwrap();
293
294 // run gadget step
295 let z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap();
296 let external_inputs_var =
297 Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(external_inputs.clone())).unwrap();
298 let z_i1_var = circom_fcircuit
299 .generate_step_constraints(cs.clone(), 1, z_i_var, external_inputs_var)
300 .unwrap();
301
302 assert_eq!(z_i1_var.value().unwrap(), z_i1_native);
303
304 // re-init cs and run gadget step with wrong ivc inputs (first ivc should not be zero)
305 let cs = ConstraintSystem::<Fr>::new_ref();
306 let wrong_z_i = vec![Fr::from(0)];
307 let wrong_z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(wrong_z_i)).unwrap();
308 let external_inputs_var =
309 Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(external_inputs)).unwrap();
310 let _z_i1_var = circom_fcircuit.generate_step_constraints(
311 cs.clone(),
312 1,
313 wrong_z_i_var,
314 external_inputs_var,
315 );
316 // TODO:: https://github.com/privacy-scaling-explorations/sonobe/issues/104
317 // Disable check for now
318 // assert!(z_i1_var.is_err());
319 }
320
321 #[test]
322 fn test_circom_no_external_inputs() {
323 let r1cs_path = PathBuf::from("./src/frontend/circom/test_folder/no_external_inputs.r1cs");
324 let wasm_path = PathBuf::from(
325 "./src/frontend/circom/test_folder/no_external_inputs_js/no_external_inputs.wasm",
326 );
327 let circom_fcircuit =
328 CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 3, 0)).unwrap();
329 let cs = ConstraintSystem::<Fr>::new_ref();
330 let z_i = vec![Fr::from(3u32), Fr::from(4u32), Fr::from(5u32)];
331 let z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i.clone())).unwrap();
332
333 // run native step
334 let z_i1_native = circom_fcircuit.step_native(1, z_i.clone(), vec![]).unwrap();
335
336 // run gadget step
337 let z_i1_var = circom_fcircuit
338 .generate_step_constraints(cs.clone(), 1, z_i_var, vec![])
339 .unwrap();
340
341 assert_eq!(z_i1_var.value().unwrap(), z_i1_native);
342
343 // re-init cs and run gadget step with wrong ivc inputs (first ivc input should not be zero)
344 let cs = ConstraintSystem::<Fr>::new_ref();
345 let wrong_z_i = vec![Fr::from(0u32), Fr::from(4u32), Fr::from(5u32)];
346 let wrong_z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(wrong_z_i)).unwrap();
347 let _z_i1_var =
348 circom_fcircuit.generate_step_constraints(cs.clone(), 1, wrong_z_i_var, vec![]);
349 // TODO:: https://github.com/privacy-scaling-explorations/sonobe/issues/104
350 // Disable check for now
351 // assert!(z_i1_var.is_err())
352 }
353
354 #[test]
355 fn test_custom_code() {
356 let r1cs_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit.r1cs");
357 let wasm_path =
358 PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
359
360 let mut circom_fcircuit =
361 CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
362
363 circom_fcircuit.set_custom_step_native(Rc::new(|_i, z_i, _external| {
364 let z = z_i[0];
365 Ok(vec![z * z * z + z + Fr::from(5)])
366 }));
367
368 // Allocates z_i1 by using step_native function.
369 let z_i = vec![Fr::from(3_u32)];
370 let wrapper_circuit = crate::frontend::utils::WrapperCircuit {
371 FC: circom_fcircuit.clone(),
372 z_i: Some(z_i.clone()),
373 z_i1: Some(circom_fcircuit.step_native(0, z_i.clone(), vec![]).unwrap()),
374 };
375
376 let cs = ConstraintSystem::<Fr>::new_ref();
377
378 wrapper_circuit.generate_constraints(cs.clone()).unwrap();
379 assert!(
380 cs.is_satisfied().unwrap(),
381 "Constraint system is not satisfied"
382 );
383 }
384}