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