diff --git a/.github/scripts/wasm-target-test-build.sh b/.github/scripts/wasm-target-test-build.sh index 64b9ba0a..09f3917b 100644 --- a/.github/scripts/wasm-target-test-build.sh +++ b/.github/scripts/wasm-target-test-build.sh @@ -15,8 +15,7 @@ cp "${GIT_ROOT}/rust-toolchain" . rustup target add wasm32-unknown-unknown wasm32-wasi # add dependencies -cargo add --path "${GIT_ROOT}/frontends" --features wasm, parallel -cargo add --path "${GIT_ROOT}/folding-schemes" --features parallel +cargo add --path "${GIT_ROOT}/folding-schemes" --features circom-browser cargo add getrandom --features js --target wasm32-unknown-unknown # test build for wasm32-* targets diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0319acd9..92a4f44b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: -p frontends --no-default-features --target ${{ matrix.target }} --features "wasm, parallel" + args: -p frontends --no-default-features --target ${{ matrix.target }} --features "circom-browser, parallel" - name: Wasm-compat folding-schemes build uses: actions-rs/cargo@v1 with: @@ -167,7 +167,7 @@ jobs: features: --features default,light-test # We only want to test `frontends` package with `wasm` feature. - feature_set: wasm - features: -p frontends --features wasm,parallel --target wasm32-unknown-unknown + features: -p frontends --features circom-browser,parallel --target wasm32-unknown-unknown steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml index 22b074de..57f0d716 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -26,17 +26,19 @@ sha3 = "0.10" log = "0.4" # tmp import for espresso's sumcheck -espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"} +espresso_subroutines = { git = "https://github.com/EspressoSystems/hyperplonk", package = "subroutines" } +# Dependencies needed for browser folding +byteorder = { version = "1.4.3", optional = true } [dev-dependencies] -ark-pallas = {version="0.4.0", features=["r1cs"]} -ark-vesta = {version="0.4.0", features=["r1cs"]} -ark-bn254 = {version="0.4.0", features=["r1cs"]} -ark-grumpkin = {version="0.4.0", features=["r1cs"]} +ark-pallas = { version = "0.4.0", features = ["r1cs"] } +ark-vesta = { version = "0.4.0", features = ["r1cs"] } +ark-bn254 = { version = "0.4.0", features = ["r1cs"] } +ark-grumpkin = { version = "0.4.0", features = ["r1cs"] } # Note: do not use the MNTx_298 curves in practice due security reasons, here # we only use them in the tests. -ark-mnt4-298 = {version="0.4.0", features=["r1cs"]} -ark-mnt6-298 = {version="0.4.0", features=["r1cs"]} +ark-mnt4-298 = { version = "0.4.0", features = ["r1cs"] } +ark-mnt6-298 = { version = "0.4.0", features = ["r1cs"] } rand = "0.8.5" num-bigint = {version = "0.4", features = ["rand"]} tracing = { version = "0.1", default-features = false, features = [ "attributes" ] } @@ -64,3 +66,6 @@ path = "../examples/multi_inputs.rs" [[example]] name = "external_inputs" path = "../examples/external_inputs.rs" + +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs index b657b61e..2697d95f 100644 --- a/folding-schemes/src/folding/nova/circuits.rs +++ b/folding-schemes/src/folding/nova/circuits.rs @@ -410,6 +410,9 @@ where // get z_{i+1} from the F circuit let i_usize = self.i_usize.unwrap_or(0); + + // If we are in the circom-browser-frontend case. The witness is already loaded within + // self.F.witness. This was done at `self.prove_step()` fn. let z_i1 = self .F .generate_step_constraints(cs.clone(), i_usize, z_i, external_inputs)?; diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index d67c33b9..1cb7b208 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -642,10 +642,36 @@ where fn prove_step( &mut self, mut rng: impl RngCore, + // This contains the full witness when we're in the circom-browser-frontend case external_inputs: Vec, // Nova does not support multi-instances folding _other_instances: Option, ) -> Result<(), Error> { + #[cfg(feature = "circom-browser")] + let external_inputs = { + // Slice and separate between external inputs and frontend witness. + let (frontend_witness, external_inputs) = + if external_inputs.len() > self.F.external_inputs_len() { + ( + // Full circom witness trace + Some(external_inputs[..].to_vec()), + // Extracted external inputs from circom trace + external_inputs[1 + self.F.state_len() * 2 + ..1 + self.F.state_len() * 2 + self.F.external_inputs_len()] + .to_vec(), + ) + } else { + (None, external_inputs) + }; + + // If we are in the circom-browser-case (frontend_witness = Some(witness)) then we load the witness into the FCircuit. + if let Some(witness) = frontend_witness { + self.F.load_witness(witness) + }; + + external_inputs + }; + // ensure that commitments are blinding if user has specified so. if H && self.i >= C1::ScalarField::one() { let blinding_commitments = if self.i == C1::ScalarField::one() { diff --git a/folding-schemes/src/frontend/mod.rs b/folding-schemes/src/frontend/mod.rs index 8570c818..ddeee4e8 100644 --- a/folding-schemes/src/frontend/mod.rs +++ b/folding-schemes/src/frontend/mod.rs @@ -45,6 +45,13 @@ pub trait FCircuit: Clone + Debug { z_i: Vec>, external_inputs: Vec>, // inputs that are not part of the state ) -> Result>, SynthesisError>; + + /// Allows to load pre-generated witness into the FCircuit implementor. + /// This is only needed by the circom-browser use cases where we have already computed our + /// witness there. And we need a way to load it into the FCircuit since it's already computed. + /// + /// By default this method will simply do nothing. Only in the circom-browser FCircuit implementors this will have usage. + fn load_witness(&mut self, _witness: Vec) {} } #[cfg(test)] diff --git a/folding-schemes/src/utils/espresso/sum_check/prover.rs b/folding-schemes/src/utils/espresso/sum_check/prover.rs index 24440d24..007a1cc4 100644 --- a/folding-schemes/src/utils/espresso/sum_check/prover.rs +++ b/folding-schemes/src/utils/espresso/sum_check/prover.rs @@ -102,14 +102,15 @@ impl SumCheckProver for IOPProverState { self.challenges.push(*chal); let r = self.challenges[self.round - 1]; - // #[cfg(feature = "parallel")] + #[cfg(feature = "parallel")] flattened_ml_extensions .par_iter_mut() .for_each(|mle| *mle = fix_variables(mle, &[r])); - // #[cfg(not(feature = "parallel"))] - // flattened_ml_extensions - // .iter_mut() - // .for_each(|mle| *mle = fix_variables(mle, &[r])); + + #[cfg(not(feature = "parallel"))] + flattened_ml_extensions + .iter_mut() + .for_each(|mle| *mle = fix_variables(mle, &[r])); } else if self.round > 0 { return Err(PolyIOPErrors::InvalidProver( "verifier message is empty".to_string(), diff --git a/frontends/src/circom/browser.rs b/frontends/src/circom/browser.rs new file mode 100644 index 00000000..9c3ac20f --- /dev/null +++ b/frontends/src/circom/browser.rs @@ -0,0 +1,169 @@ +use crate::frontend::FCircuit; +use crate::frontend::FpVar::Var; +use crate::Error; +use ark_circom::circom::{CircomCircuit, R1CS as CircomR1CS}; +use ark_circom::circom::{R1CSFile, R1CS}; +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::alloc::AllocVar; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::fmt::Debug; +use byteorder::{LittleEndian, ReadBytesExt}; +use std::io::{BufReader, Cursor, Read}; +use std::usize; + +/// This circuit is the one we will use in order to fold from the browser. +/// +/// A clear example on how to do all of this can be seen at: +/// +#[derive(Clone, Debug)] +pub struct CircomFCircuitBrowser { + pub state_len: usize, + pub external_inputs_len: usize, + pub witness: Vec, + pub r1cs: CircomR1CS, +} + +impl FCircuit for CircomFCircuitBrowser { + /// (r1cs_file_bytes, state_len, external_inputs_len) + type Params = (Vec, usize, usize); + + fn new(params: Self::Params) -> Result { + let (r1cs_bytes, state_len, external_inputs_len) = params; + let reader = BufReader::new(Cursor::new(&r1cs_bytes[..])); + + let mut r1cs: R1CS = R1CSFile::::new(reader) + .expect("Error reading R1cs") + .into(); + + // That's some weird magic. Ask @dmpierre + r1cs.wire_mapping = None; + + Ok(Self { + state_len, + external_inputs_len, + witness: vec![F::zero(); r1cs.num_variables], + r1cs, + }) + } + + fn state_len(&self) -> usize { + self.state_len + } + fn external_inputs_len(&self) -> usize { + self.external_inputs_len + } + + fn step_native( + &self, + _i: usize, + z_i: Vec, + external_inputs: Vec, + ) -> Result, Error> { + // Should we keep these? + assert_eq!(z_i.len(), self.state_len()); + assert_eq!(external_inputs.len(), self.external_inputs_len()); + + // Extracts the z_i1(next state) from the witness vector and external inputs. + let z_i1 = z_i[1..1 + self.state_len()].to_vec(); + Ok(z_i1) + } + + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: usize, + z_i: Vec>, + _external_inputs: Vec>, + ) -> Result>, SynthesisError> { + // Since public inputs are already allocated variables, we will tell `circom-compat` to not re-allocate those + let mut already_allocated_public_inputs = vec![]; + for var in z_i.iter() { + match var { + Var(var) => already_allocated_public_inputs.push(var.variable), + _ => return Err(SynthesisError::Unsatisfiable), // allocated z_i should be Var + } + } + + // Initializes the CircomCircuit. + let circom_circuit = CircomCircuit { + r1cs: self.r1cs.clone(), + witness: Some(self.witness.clone()), + public_inputs_indexes: already_allocated_public_inputs, + // Since public inputs are already allocated variables, we will tell `circom-compat` to not re-allocate those + allocate_inputs_as_witnesses: true, + }; + + // Generates the constraints for the circom_circuit. + circom_circuit.generate_constraints(cs.clone())?; + + // Extracts the z_i1(next state) from the witness vector. + let z_i1: Vec> = Vec::>::new_witness(cs.clone(), || { + Ok(self.witness[1..1 + self.state_len()].to_vec()) + })?; + + Ok(z_i1) + } + + fn load_witness(&mut self, witness: Vec) { + self.witness = witness; + } +} + +/// Load Circom-generated witness from u8 array by a reader. +/// +/// This fn has been taken from +pub fn load_witness_from_bin_reader(mut reader: R) -> Vec { + let mut wtns_header = [0u8; 4]; + reader.read_exact(&mut wtns_header).expect("read_error"); + if wtns_header != [119, 116, 110, 115] { + panic!("invalid file header"); + } + let version = reader.read_u32::().expect("read_error"); + if version > 2 { + panic!("unsupported file version"); + } + let num_sections = reader.read_u32::().expect("read_error"); + if num_sections != 2 { + panic!("invalid num sections"); + } + // read the first section + let sec_type = reader.read_u32::().expect("read_error"); + if sec_type != 1 { + panic!("invalid section type"); + } + let sec_size = reader.read_u64::().expect("read_error"); + if sec_size != 4 + 32 + 4 { + panic!("invalid section len") + } + let field_size: u32 = reader.read_u32::().expect("read_error"); + if field_size != 32 { + panic!("invalid field byte size"); + } + let mut prime = vec![0u8; field_size as usize]; + reader.read_exact(&mut prime).expect("read_error"); + let witness_len: u32 = reader.read_u32::().expect("read_error"); + let sec_type = reader.read_u32::().expect("read_error"); + if sec_type != 2 { + panic!("invalid section type"); + } + let sec_size = reader.read_u64::().expect("read_error"); + if sec_size != (witness_len * field_size) as u64 { + panic!("invalid witness section size"); + } + let mut result = Vec::with_capacity(witness_len as usize); + for _ in 0..witness_len { + result.push(read_field::<&mut R, F>(&mut reader).expect("read_error")); + } + result +} + +fn read_field( + mut reader: R, +) -> Result { + let mut repr: Vec = F::ZERO.into_bigint().to_bytes_le(); + for digit in repr.iter_mut() { + *digit = reader.read_u8()?; + } + Ok(F::from_le_bytes_mod_order(&repr[..])) +} diff --git a/frontends/src/circom/mod.rs b/frontends/src/circom/mod.rs index 88a9da5b..e118c7ef 100644 --- a/frontends/src/circom/mod.rs +++ b/frontends/src/circom/mod.rs @@ -11,7 +11,12 @@ use num_bigint::BigInt; use std::rc::Rc; use std::{fmt, usize}; +#[cfg(feature = "circom-browser")] +pub mod browser; pub mod utils; + +#[cfg(feature = "circom-browser")] +pub use browser::{load_witness_from_bin_reader, CircomFCircuitBrowser}; use utils::CircomWrapper; type ClosurePointer = Rc, Vec) -> Result, Error>>; @@ -31,7 +36,8 @@ impl fmt::Debug for CustomStepNative { } } -/// Define CircomFCircuit +/// This circuit is the one we will use in order to fold Circom circuits +/// from a non-browser environment. #[derive(Clone, Debug)] pub struct CircomFCircuit { circom_wrapper: CircomWrapper,