Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/better browser support #169

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
66 changes: 47 additions & 19 deletions folding-schemes/Cargo.toml
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the formatting has some strange behaviour? Line 53 has 146 characters, while wrapping happens at line 78 at line 27.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Unsure why this happens TBH. Let me try to fix it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope 30a78d3 looks better

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to solve it and makes the parser unhappy...

I don't think I can do much with this..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related: #169 (comment)

Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,40 @@ version = "0.1.0"
edition = "2021"

[dependencies]
ark-ec = { version = "^0.4.0", default-features = false, features = ["parallel"] }
ark-ff = { version = "^0.4.0", default-features = false, features = ["parallel", "asm"] }
ark-poly = { version = "^0.4.0", default-features = false, features = ["parallel"] }
ark-std = { version = "^0.4.0", default-features = false, features = ["parallel"] }
ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = ["r1cs", "sponge", "crh", "parallel"] }
ark-ec = { version = "^0.4.0", default-features = false, features = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, most of the changes in this file are formatting (mostly converting one-lines into multi-lines). Is there some standard that we could use, and if so could we apply it in the CI (and in our machines too) like we do with the rustfmt?
(the split into lines make it harder to read tho)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it seems arbitrary as @dmpierre pointed in the comment #169 (comment)
(some shorter lines get split, while longer ones keep as a oneline)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VSCode did that for some reason I ignore..

Happy to bring this to the previous status or use whatever tool is suggested bu you guys.

"parallel",
] }
ark-ff = { version = "^0.4.0", default-features = false, features = [
"parallel",
"asm",
] }
ark-poly = { version = "^0.4.0", default-features = false, features = [
"parallel",
] }
ark-std = { version = "^0.4.0", default-features = false, features = [
"parallel",
] }
ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = [
"r1cs",
"sponge",
"crh",
"parallel",
] }
ark-grumpkin = { version = "0.4.0", default-features = false }
ark-poly-commit = { version = "^0.4.0", default-features = false, features = ["parallel"] }
ark-poly-commit = { version = "^0.4.0", default-features = false, features = [
"parallel",
] }
ark-relations = { version = "^0.4.0", default-features = false }
# this is patched at the workspace level
ark-r1cs-std = { version = "0.4.0", default-features = false, features = ["parallel"] }
ark-r1cs-std = { version = "0.4.0", default-features = false, features = [
"parallel",
] }
ark-snark = { version = "^0.4.0", default-features = false }
ark-serialize = { version = "^0.4.0", default-features = false }
ark-circom = { git = "https://github.com/arnaucube/circom-compat", default-features = false }
ark-groth16 = { version = "^0.4.0", default-features = false, features = ["parallel"]}
ark-groth16 = { version = "^0.4.0", default-features = false, features = [
"parallel",
] }
ark-bn254 = { version = "^0.4.0", default-features = false }
thiserror = "1.0"
rayon = "1"
Expand All @@ -27,26 +47,30 @@ color-eyre = "=0.6.2"
sha3 = "0.10"
ark-noname = { git = "https://github.com/dmpierre/ark-noname", branch = "feat/sonobe-integration" }
noname = { git = "https://github.com/dmpierre/noname" }
serde_json = "1.0.85" # to (de)serialize JSON
serde_json = "1.0.85" # to (de)serialize JSON
serde = "1.0.203"
acvm = { git = "https://github.com/noir-lang/noir", rev="2b4853e", default-features = false }
noir_arkworks_backend = { package="arkworks_backend", git = "https://github.com/dmpierre/arkworks_backend", branch = "feat/sonobe-integration" }
acvm = { git = "https://github.com/noir-lang/noir", rev = "2b4853e", default-features = false }
noir_arkworks_backend = { package = "arkworks_backend", git = "https://github.com/dmpierre/arkworks_backend", branch = "feat/sonobe-integration" }
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"
tracing = { version = "0.1", default-features = false, features = [ "attributes" ] }
tracing = { version = "0.1", default-features = false, features = [
"attributes",
] }
tracing-subscriber = { version = "0.2" }

# This allows the crate to be built when targeting WASM.
Expand All @@ -58,6 +82,7 @@ getrandom = { version = "0.2", features = ["js"] }
default = ["ark-circom/default", "parallel"]
parallel = []
wasm = ["ark-circom/wasm"]
browser = ["wasm", "byteorder"]
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
light-test = []


Expand All @@ -72,3 +97,6 @@ path = "../examples/multi_inputs.rs"
[[example]]
name = "external_inputs"
path = "../examples/external_inputs.rs"

[lib]
crate-type = ["cdylib", "rlib"]
3 changes: 3 additions & 0 deletions folding-schemes/src/folding/nova/circuits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 browser-frontend case. The witness is already loaded within
// self.F.witness. This was done at `self.prove_step()` fn.
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
let z_i1 = self
.F
.generate_step_constraints(cs.clone(), i_usize, z_i, external_inputs)?;
Expand Down
21 changes: 21 additions & 0 deletions folding-schemes/src/folding/nova/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,10 +642,31 @@ where
fn prove_step(
&mut self,
mut rng: impl RngCore,
// This contains the full witness when we're in the browser-frontend case
external_inputs: Vec<C1::ScalarField>,
// Nova does not support multi-instances folding
_other_instances: Option<Self::MultiCommittedInstanceWithWitness>,
) -> Result<(), Error> {
// 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 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)
};

CPerezz marked this conversation as resolved.
Show resolved Hide resolved
// 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() {
Expand Down
177 changes: 177 additions & 0 deletions folding-schemes/src/frontend/circom/browser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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:
/// <https://github.com/CPerezz/wasm-sonobe-integration>
#[derive(Clone, Debug)]
pub struct CircomFCircuitBrowser<F: PrimeField> {
pub state_len: usize,
pub external_inputs_len: usize,
pub witness: Vec<F>,
pub r1cs: CircomR1CS<F>,
}

impl<F: PrimeField> FCircuit<F> for CircomFCircuitBrowser<F> {
/// (r1cs_file_bytes, state_len, external_inputs_len)
type Params = (Vec<u8>, usize, usize);

fn new(params: Self::Params) -> Result<Self, Error> {
let (r1cs_bytes, state_len, external_inputs_len) = params;
let reader = BufReader::new(Cursor::new(&r1cs_bytes[..]));

let mut r1cs: R1CS<F> = R1CSFile::<F>::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<F>,
external_inputs: Vec<F>,
) -> Result<Vec<F>, Error> {
// Should we keep these?
assert_eq!(z_i.len(), self.state_len());
assert_eq!(external_inputs.len(), self.external_inputs_len());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this can be changed into an 'if' that returns an error instead of panicking? so that the user of the lib does not have an uncontrolled crash of the run but an error that they can decide how to manage.

Or at least under a test flag (#[cfg(test)]) as in the original Circom frontend folding-schemes/src/frontend/circom/mod.rs#L60

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem that I want to avoid is having errors. Errors are really hard to optimize for whereas panics aren't.

Also, this assert should only trigger when you're testing. In prod, you should always have a way to ensure the lengths are correct. (Notice that your'e not going to change the circuit for which you're generating witness nor anything similar.

If honest, I'd entirely remove the panics and errors and simply do panic="abort in the profile.
But, for now I left the panics so that debugging is easier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or at least under a test flag (#[cfg(test)]) as in the original Circom frontend folding-schemes/src/frontend/circom/mod.rs#L60

Notice that under a testing flag, this will never be triggered. As you will need to compile the lib for testing to then compile also the tests within the WASM binary.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the test flag it will still be triggered when running the tests, no? (previous to going into wasm in the browser)

The point of those checks is not to panic the runtime of the program that uses sonobe, but to make the dev aware that the lenghts don't match.
With the assert_eq the app breaks without the dev being able to recover.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, should we add an error then? Or simply ignore?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we can do it as in the lines linked in this comment: #169 (comment)


// Extracts the z_i1(next state) from the witness vector and external inputs.
let z_i1 = z_i[..self.state_len()].to_vec();
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
Ok(z_i1)
}

fn generate_step_constraints(
&self,
cs: ConstraintSystemRef<F>,
_i: usize,
z_i: Vec<FpVar<F>>,
// This in reality contains the `witness` passed from the `prove_step` call from the
// browser.
_external_inputs: Vec<FpVar<F>>,
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<Vec<FpVar<F>>, 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<FpVar<F>> = Vec::<FpVar<F>>::new_witness(cs.clone(), || {
Ok(self.witness[1..1 + self.state_len()].to_vec())
})?;

Ok(z_i1)
}

fn load_witness(&mut self, witness: Vec<F>) {
self.witness = witness;
}
}

/// Load Circom-generated witness from u8 array by a reader.
///
/// This fn has been taken from <https://github.com/nalinbhardwaj/Nova-Scotia/blob/main/src/circom/file.rs>
pub fn load_witness_from_bin_reader<R: Read, F: PrimeField>(mut reader: R) -> Vec<F> {
let mut wtns_header = [0u8; 4];
reader.read_exact(&mut wtns_header).expect("read_error");
if wtns_header != [119, 116, 110, 115] {
// ruby -e 'p "wtns".bytes' => [119, 116, 110, 115]
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
panic!("invalid file header");
}
let version = reader.read_u32::<LittleEndian>().expect("read_error");
// println!("wtns version {}", version);
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
if version > 2 {
panic!("unsupported file version");
}
let num_sections = reader.read_u32::<LittleEndian>().expect("read_error");
if num_sections != 2 {
panic!("invalid num sections");
}
// read the first section
let sec_type = reader.read_u32::<LittleEndian>().expect("read_error");
if sec_type != 1 {
panic!("invalid section type");
}
let sec_size = reader.read_u64::<LittleEndian>().expect("read_error");
if sec_size != 4 + 32 + 4 {
panic!("invalid section len")
}
let field_size: u32 = reader.read_u32::<LittleEndian>().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");
// if prime != hex!("010000f093f5e1439170b97948e833285d588181b64550b829a031e1724e6430") {
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
// panic!("invalid curve prime {:?}", prime);
// }
arnaucube marked this conversation as resolved.
Show resolved Hide resolved
let witness_len: u32 = reader.read_u32::<LittleEndian>().expect("read_error");
// println!("witness len {}", witness_len);
let sec_type = reader.read_u32::<LittleEndian>().expect("read_error");
if sec_type != 2 {
panic!("invalid section type");
}
let sec_size = reader.read_u64::<LittleEndian>().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<R: Read, F: PrimeField>(
mut reader: R,
) -> Result<F, ark_serialize::SerializationError> {
let mut repr: Vec<u8> = 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[..]))
}
8 changes: 7 additions & 1 deletion folding-schemes/src/frontend/circom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ use num_bigint::BigInt;
use std::rc::Rc;
use std::{fmt, usize};

#[cfg(feature = "browser")]
pub mod browser;
pub mod utils;

#[cfg(feature = "browser")]
pub use browser::{load_witness_from_bin_reader, CircomFCircuitBrowser};
use utils::CircomWrapper;

type ClosurePointer<F> = Rc<dyn Fn(usize, Vec<F>, Vec<F>) -> Result<Vec<F>, Error>>;
Expand All @@ -33,7 +38,8 @@ impl<F: PrimeField> fmt::Debug for CustomStepNative<F> {
}
}

/// 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<F: PrimeField> {
circom_wrapper: CircomWrapper<F>,
Expand Down
7 changes: 7 additions & 0 deletions folding-schemes/src/frontend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub trait FCircuit<F: PrimeField>: Clone + Debug {
z_i: Vec<FpVar<F>>,
external_inputs: Vec<FpVar<F>>, // inputs that are not part of the state
) -> Result<Vec<FpVar<F>>, SynthesisError>;

/// Allows to load pre-generated witness into the FCircuit implementor.
/// This is only needed by the 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 browser FCircuit implementors this will have usage.
fn load_witness(&mut self, _witness: Vec<F>) {}
}

#[cfg(test)]
Expand Down
Loading