sonobe/src/frontend/circom/mod.rs
src/frontend/circom/mod.rsRaw
1use std::{error::Error, fs::File, io::BufReader, marker::PhantomData, path::PathBuf};
2
3use color_eyre::Result;
4use num_bigint::BigInt;
5
6use ark_circom::{circom::r1cs_reader, WitnessCalculator};
7use ark_ec::pairing::Pairing;
8use ark_ff::PrimeField;
9
10use crate::ccs::r1cs::R1CS;
11use crate::utils::vec::SparseMatrix;
12
13// Define the sparse matrices on PrimeFiled.
14pub type Constraints<F> = (ConstraintVec<F>, ConstraintVec<F>, ConstraintVec<F>);
15pub type ConstraintVec<F> = Vec<(usize, F)>;
16type ExtractedConstraints<F> = (Vec<Constraints<F>>, usize, usize);
17pub type ExtractedConstraintsResult<F> = Result<ExtractedConstraints<F>, Box<dyn Error>>;
18pub type R1CSandZ<F> = (R1CS<F>, Vec<F>);
19
20// A struct that wraps Circom functionalities, allowing for extraction of R1CS and witnesses
21// based on file paths to Circom's .r1cs and .wasm.
22pub struct CircomWrapper<E: Pairing> {
23 r1cs_filepath: PathBuf,
24 wasm_filepath: PathBuf,
25 _marker: PhantomData<E>,
26}
27
28impl<E: Pairing> CircomWrapper<E> {
29 // Creates a new instance of the CircomWrapper with the file paths.
30 pub fn new(r1cs_filepath: PathBuf, wasm_filepath: PathBuf) -> Self {
31 CircomWrapper {
32 r1cs_filepath,
33 wasm_filepath,
34 _marker: PhantomData,
35 }
36 }
37
38 // Aggregates multiple functions to obtain R1CS and Z as defined in folding-schemes from Circom.
39 pub fn extract_r1cs_and_z(
40 &self,
41 inputs: &[(String, Vec<BigInt>)],
42 ) -> Result<R1CSandZ<E::ScalarField>, Box<dyn Error>> {
43 let (constraints, pub_io_len, num_variables) = self.extract_constraints_from_r1cs()?;
44 let witness = self.calculate_witness(inputs)?;
45 self.circom_to_folding_schemes_r1cs_and_z(constraints, &witness, pub_io_len, num_variables)
46 }
47
48 // Extracts constraints from the r1cs file.
49 pub fn extract_constraints_from_r1cs(&self) -> ExtractedConstraintsResult<E::ScalarField>
50 where
51 E: Pairing,
52 {
53 // Opens the .r1cs file and create a reader.
54 let file = File::open(&self.r1cs_filepath)?;
55 let reader = BufReader::new(file);
56
57 // Reads the R1CS file and extract the constraints directly.
58 let r1cs_file = r1cs_reader::R1CSFile::<E>::new(reader)?;
59 let pub_io_len = (r1cs_file.header.n_pub_in + r1cs_file.header.n_pub_out) as usize;
60 let r1cs = r1cs_reader::R1CS::<E>::from(r1cs_file);
61 let num_variables = r1cs.num_variables;
62 let constraints: Vec<Constraints<E::ScalarField>> = r1cs.constraints;
63
64 Ok((constraints, pub_io_len, num_variables))
65 }
66
67 // Converts a set of constraints from ark-circom into R1CS format of folding-schemes.
68 pub fn convert_to_folding_schemes_r1cs<F>(
69 &self,
70 constraints: Vec<Constraints<F>>,
71 pub_io_len: usize,
72 num_variables: usize,
73 ) -> R1CS<F>
74 where
75 F: PrimeField,
76 {
77 let mut a_matrix: Vec<Vec<(F, usize)>> = Vec::new();
78 let mut b_matrix: Vec<Vec<(F, usize)>> = Vec::new();
79 let mut c_matrix: Vec<Vec<(F, usize)>> = Vec::new();
80
81 let n_rows = constraints.len();
82
83 for (ai, bi, ci) in constraints {
84 a_matrix.push(
85 ai.into_iter()
86 .map(|(index, scalar)| (scalar, index))
87 .collect(),
88 );
89 b_matrix.push(
90 bi.into_iter()
91 .map(|(index, scalar)| (scalar, index))
92 .collect(),
93 );
94 c_matrix.push(
95 ci.into_iter()
96 .map(|(index, scalar)| (scalar, index))
97 .collect(),
98 );
99 }
100
101 let l = pub_io_len;
102 let n_cols = num_variables;
103
104 let A = SparseMatrix {
105 n_rows,
106 n_cols,
107 coeffs: a_matrix,
108 };
109 let B = SparseMatrix {
110 n_rows,
111 n_cols,
112 coeffs: b_matrix,
113 };
114 let C = SparseMatrix {
115 n_rows,
116 n_cols,
117 coeffs: c_matrix,
118 };
119
120 R1CS::<F> { l, A, B, C }
121 }
122
123 // Calculates the witness given the Wasm filepath and inputs.
124 pub fn calculate_witness(&self, inputs: &[(String, Vec<BigInt>)]) -> Result<Vec<BigInt>> {
125 let mut calculator = WitnessCalculator::new(&self.wasm_filepath)?;
126 calculator.calculate_witness(inputs.iter().cloned(), true)
127 }
128
129 // Converts a num_bigint input to `PrimeField`'s BigInt.
130 pub fn num_bigint_to_ark_bigint<F: PrimeField>(
131 &self,
132 value: &BigInt,
133 ) -> Result<F::BigInt, Box<dyn Error>> {
134 let big_uint = value
135 .to_biguint()
136 .ok_or_else(|| "BigInt is negative".to_string())?;
137 F::BigInt::try_from(big_uint).map_err(|_| "BigInt conversion failed".to_string().into())
138 }
139
140 // Converts R1CS constraints and witness from ark-circom format
141 // into folding-schemes R1CS and z format.
142 pub fn circom_to_folding_schemes_r1cs_and_z<F>(
143 &self,
144 constraints: Vec<Constraints<F>>,
145 witness: &[BigInt],
146 pub_io_len: usize,
147 num_variables: usize,
148 ) -> Result<(R1CS<F>, Vec<F>), Box<dyn Error>>
149 where
150 F: PrimeField,
151 {
152 let folding_schemes_r1cs =
153 self.convert_to_folding_schemes_r1cs(constraints, pub_io_len, num_variables);
154
155 let z: Result<Vec<F>, _> = witness
156 .iter()
157 .map(|big_int| {
158 let ark_big_int = self.num_bigint_to_ark_bigint::<F>(big_int)?;
159 F::from_bigint(ark_big_int).ok_or_else(|| {
160 "Failed to convert bigint to field element"
161 .to_string()
162 .into()
163 })
164 })
165 .collect();
166
167 z.map(|z| (folding_schemes_r1cs, z))
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use crate::frontend::circom::CircomWrapper;
174 use ark_bn254::Bn254;
175 use num_bigint::BigInt;
176 use std::env;
177
178 /*
179 test_circuit represents 35 = x^3 + x + 5 in test_folder/test_circuit.circom.
180 In the test_circom_conversion_success function, x is assigned the value 3, which satisfies the R1CS correctly.
181 In the test_circom_conversion_failure function, x is assigned the value 6, which doesn't satisfy the R1CS.
182 */
183
184 /*
185 To generate .r1cs and .wasm files, run the below command in the terminal.
186 bash ./src/frontend/circom/test_folder/compile.sh
187 */
188
189 fn test_circom_conversion_logic(expect_success: bool, inputs: Vec<(String, Vec<BigInt>)>) {
190 let current_dir = env::current_dir().unwrap();
191 let base_path = current_dir.join("src/frontend/circom/test_folder");
192
193 let r1cs_filepath = base_path.join("test_circuit.r1cs");
194 let wasm_filepath = base_path.join("test_circuit_js/test_circuit.wasm");
195
196 assert!(r1cs_filepath.exists());
197 assert!(wasm_filepath.exists());
198
199 let circom_wrapper = CircomWrapper::<Bn254>::new(r1cs_filepath, wasm_filepath);
200
201 let (r1cs, z) = circom_wrapper
202 .extract_r1cs_and_z(&inputs)
203 .expect("Error processing input");
204
205 // Checks the relationship of R1CS.
206 let check_result = std::panic::catch_unwind(|| {
207 r1cs.check_relation(&z).unwrap();
208 });
209
210 match (expect_success, check_result) {
211 (true, Ok(_)) => {}
212 (false, Err(_)) => {}
213 (true, Err(_)) => panic!("Expected success but got a failure."),
214 (false, Ok(_)) => panic!("Expected a failure but got success."),
215 }
216 }
217
218 #[test]
219 fn test_circom_conversion() {
220 // expect it to pass for correct inputs.
221 test_circom_conversion_logic(true, vec![("step_in".to_string(), vec![BigInt::from(3)])]);
222
223 // expect it to fail for incorrect inputs.
224 test_circom_conversion_logic(false, vec![("step_in".to_string(), vec![BigInt::from(7)])]);
225 }
226}