diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfbfde4..d8e8e3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,14 @@ jobs: # use the more efficient nextest - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 + - name: Download Circom + run: | + mkdir -p $HOME/bin + curl -sSfL https://github.com/iden3/circom/releases/download/v2.1.6/circom-linux-amd64 -o $HOME/bin/circom + chmod +x $HOME/bin/circom + echo "$HOME/bin" >> $GITHUB_PATH + - name: Execute compile.sh to generate .r1cs and .wasm from .circom + run: bash ./src/frontend/circom/test_folder/compile.sh - name: Build # This build will be reused by nextest, # and also checks (--all-targets) that benches don't bit-rot diff --git a/.gitignore b/.gitignore index 869df07..e8d5982 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /target -Cargo.lock \ No newline at end of file +Cargo.lock +# Circom generated files +/src/frontend/circom/test_folder/test_circuit.r1cs +/src/frontend/circom/test_folder/test_circuit_js/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 686dbf1..69f276b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,25 +4,28 @@ version = "0.1.0" edition = "2021" [dependencies] -ark-ec = "0.4.2" +ark-ec = "^0.4.0" ark-ff = "^0.4.0" ark-poly = "^0.4.0" ark-std = "^0.4.0" ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = ["r1cs", "sponge", "crh"] } ark-relations = { version = "^0.4.0", default-features = false } ark-r1cs-std = { version = "^0.4.0", default-features = false } +ark-circom = { git = "https://github.com/gakonst/ark-circom.git" } thiserror = "1.0" rayon = "1.7.0" +num-bigint = "0.4" +color-eyre = "=0.6.2" # tmp imports for espresso's sumcheck -ark-serialize = "0.4.2" +ark-serialize = "^0.4.0" espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"} espresso_transcript = {git="https://github.com/EspressoSystems/hyperplonk", package="transcript"} - [dev-dependencies] ark-pallas = {version="0.4.0", features=["r1cs"]} ark-vesta = {version="0.4.0"} +ark-bn254 = "0.4.0" [features] default = ["parallel", "nova", "hypernova"] diff --git a/src/frontend/circom/mod.rs b/src/frontend/circom/mod.rs new file mode 100644 index 0000000..1a8ad41 --- /dev/null +++ b/src/frontend/circom/mod.rs @@ -0,0 +1,226 @@ +use std::{error::Error, fs::File, io::BufReader, marker::PhantomData, path::PathBuf}; + +use color_eyre::Result; +use num_bigint::BigInt; + +use ark_circom::{circom::r1cs_reader, WitnessCalculator}; +use ark_ec::pairing::Pairing; +use ark_ff::PrimeField; + +use crate::ccs::r1cs::R1CS; +use crate::utils::vec::SparseMatrix; + +// Define the sparse matrices on PrimeFiled. +pub type Constraints = (ConstraintVec, ConstraintVec, ConstraintVec); +pub type ConstraintVec = Vec<(usize, F)>; +type ExtractedConstraints = (Vec>, usize, usize); +pub type ExtractedConstraintsResult = Result, Box>; +pub type R1CSandZ = (R1CS, Vec); + +// A struct that wraps Circom functionalities, allowing for extraction of R1CS and witnesses +// based on file paths to Circom's .r1cs and .wasm. +pub struct CircomWrapper { + r1cs_filepath: PathBuf, + wasm_filepath: PathBuf, + _marker: PhantomData, +} + +impl CircomWrapper { + // Creates a new instance of the CircomWrapper with the file paths. + pub fn new(r1cs_filepath: PathBuf, wasm_filepath: PathBuf) -> Self { + CircomWrapper { + r1cs_filepath, + wasm_filepath, + _marker: PhantomData, + } + } + + // Aggregates multiple functions to obtain R1CS and Z as defined in folding-schemes from Circom. + pub fn extract_r1cs_and_z( + &self, + inputs: &[(String, Vec)], + ) -> Result, Box> { + let (constraints, pub_io_len, num_variables) = self.extract_constraints_from_r1cs()?; + let witness = self.calculate_witness(inputs)?; + self.circom_to_folding_schemes_r1cs_and_z(constraints, &witness, pub_io_len, num_variables) + } + + // Extracts constraints from the r1cs file. + pub fn extract_constraints_from_r1cs(&self) -> ExtractedConstraintsResult + where + E: Pairing, + { + // Opens the .r1cs file and create a reader. + let file = File::open(&self.r1cs_filepath)?; + let reader = BufReader::new(file); + + // Reads the R1CS file and extract the constraints directly. + let r1cs_file = r1cs_reader::R1CSFile::::new(reader)?; + let pub_io_len = (r1cs_file.header.n_pub_in + r1cs_file.header.n_pub_out) as usize; + let r1cs = r1cs_reader::R1CS::::from(r1cs_file); + let num_variables = r1cs.num_variables; + let constraints: Vec> = r1cs.constraints; + + Ok((constraints, pub_io_len, num_variables)) + } + + // Converts a set of constraints from ark-circom into R1CS format of folding-schemes. + pub fn convert_to_folding_schemes_r1cs( + &self, + constraints: Vec>, + pub_io_len: usize, + num_variables: usize, + ) -> R1CS + where + F: PrimeField, + { + let mut a_matrix: Vec> = Vec::new(); + let mut b_matrix: Vec> = Vec::new(); + let mut c_matrix: Vec> = Vec::new(); + + let n_rows = constraints.len(); + + for (ai, bi, ci) in constraints { + a_matrix.push( + ai.into_iter() + .map(|(index, scalar)| (scalar, index)) + .collect(), + ); + b_matrix.push( + bi.into_iter() + .map(|(index, scalar)| (scalar, index)) + .collect(), + ); + c_matrix.push( + ci.into_iter() + .map(|(index, scalar)| (scalar, index)) + .collect(), + ); + } + + let l = pub_io_len; + let n_cols = num_variables; + + let A = SparseMatrix { + n_rows, + n_cols, + coeffs: a_matrix, + }; + let B = SparseMatrix { + n_rows, + n_cols, + coeffs: b_matrix, + }; + let C = SparseMatrix { + n_rows, + n_cols, + coeffs: c_matrix, + }; + + R1CS:: { l, A, B, C } + } + + // Calculates the witness given the Wasm filepath and inputs. + pub fn calculate_witness(&self, inputs: &[(String, Vec)]) -> Result> { + let mut calculator = WitnessCalculator::new(&self.wasm_filepath)?; + calculator.calculate_witness(inputs.iter().cloned(), true) + } + + // Converts a num_bigint input to `PrimeField`'s BigInt. + pub fn num_bigint_to_ark_bigint( + &self, + value: &BigInt, + ) -> Result> { + let big_uint = value + .to_biguint() + .ok_or_else(|| "BigInt is negative".to_string())?; + F::BigInt::try_from(big_uint).map_err(|_| "BigInt conversion failed".to_string().into()) + } + + // Converts R1CS constraints and witness from ark-circom format + // into folding-schemes R1CS and z format. + pub fn circom_to_folding_schemes_r1cs_and_z( + &self, + constraints: Vec>, + witness: &[BigInt], + pub_io_len: usize, + num_variables: usize, + ) -> Result<(R1CS, Vec), Box> + where + F: PrimeField, + { + let folding_schemes_r1cs = + self.convert_to_folding_schemes_r1cs(constraints, pub_io_len, num_variables); + + let z: Result, _> = witness + .iter() + .map(|big_int| { + let ark_big_int = self.num_bigint_to_ark_bigint::(big_int)?; + F::from_bigint(ark_big_int).ok_or_else(|| { + "Failed to convert bigint to field element" + .to_string() + .into() + }) + }) + .collect(); + + z.map(|z| (folding_schemes_r1cs, z)) + } +} + +#[cfg(test)] +mod tests { + use crate::frontend::circom::CircomWrapper; + use ark_bn254::Bn254; + use num_bigint::BigInt; + use std::env; + + /* + test_circuit represents 35 = x^3 + x + 5 in test_folder/test_circuit.circom. + In the test_circom_conversion_success function, x is assigned the value 3, which satisfies the R1CS correctly. + In the test_circom_conversion_failure function, x is assigned the value 6, which doesn't satisfy the R1CS. + */ + + /* + To generate .r1cs and .wasm files, run the below command in the terminal. + bash ./src/frontend/circom/test_folder/compile.sh + */ + + fn test_circom_conversion_logic(expect_success: bool, inputs: Vec<(String, Vec)>) { + let current_dir = env::current_dir().unwrap(); + let base_path = current_dir.join("src/frontend/circom/test_folder"); + + let r1cs_filepath = base_path.join("test_circuit.r1cs"); + let wasm_filepath = base_path.join("test_circuit_js/test_circuit.wasm"); + + assert!(r1cs_filepath.exists()); + assert!(wasm_filepath.exists()); + + let circom_wrapper = CircomWrapper::::new(r1cs_filepath, wasm_filepath); + + let (r1cs, z) = circom_wrapper + .extract_r1cs_and_z(&inputs) + .expect("Error processing input"); + + // Checks the relationship of R1CS. + let check_result = std::panic::catch_unwind(|| { + r1cs.check_relation(&z).unwrap(); + }); + + match (expect_success, check_result) { + (true, Ok(_)) => {} + (false, Err(_)) => {} + (true, Err(_)) => panic!("Expected success but got a failure."), + (false, Ok(_)) => panic!("Expected a failure but got success."), + } + } + + #[test] + fn test_circom_conversion() { + // expect it to pass for correct inputs. + test_circom_conversion_logic(true, vec![("step_in".to_string(), vec![BigInt::from(3)])]); + + // expect it to fail for incorrect inputs. + test_circom_conversion_logic(false, vec![("step_in".to_string(), vec![BigInt::from(7)])]); + } +} diff --git a/src/frontend/circom/test_folder/compile.sh b/src/frontend/circom/test_folder/compile.sh new file mode 100644 index 0000000..49c4412 --- /dev/null +++ b/src/frontend/circom/test_folder/compile.sh @@ -0,0 +1,2 @@ +#!/bin/bash +circom ./src/frontend/circom/test_folder/test_circuit.circom --r1cs --wasm --prime bn128 --output ./src/frontend/circom/test_folder/ \ No newline at end of file diff --git a/src/frontend/circom/test_folder/test_circuit.circom b/src/frontend/circom/test_folder/test_circuit.circom new file mode 100644 index 0000000..aa17932 --- /dev/null +++ b/src/frontend/circom/test_folder/test_circuit.circom @@ -0,0 +1,13 @@ +pragma circom 2.0.3; + +template Example () { + signal input step_in; + signal output step_out; + signal temp; + + temp <== step_in * step_in; + step_out <== temp * step_in + step_in + 5; + step_out === 35; +} + +component main = Example(); diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 3222a46..80a1046 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1 +1,2 @@ pub mod arkworks; +pub mod circom;