diff --git a/Cargo.toml b/Cargo.toml index 1976467..1879369 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] - -[dev-dependencies] ark-groth16 = { version = "^0.4.0" } +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-ec = "0.4.1" @@ -30,6 +30,7 @@ num-bigint = "0.4.3" # this feature (but then the DeciderETH circuit is bigger and takes more time # to compute). folding-schemes = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "folding-schemes", features=["light-test"]} +folding-schemes-circom = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "frontends", optional=true} solidity-verifiers = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "solidity-verifiers"} serde = "1.0.198" serde_json = "1.0.116" @@ -37,6 +38,15 @@ tiny-keccak = { version = "2.0", features = ["keccak"] } rand = "0.8.5" + + +[dev-dependencies] + +[features] +default = [] +experimental-frontends = ["dep:folding-schemes-circom"] + + [patch.crates-io] # patch ark_curves to use a cherry-picked version which contains # bn254::constraints & grumpkin for v0.4.0 (once arkworks v0.5.0 is released diff --git a/README.md b/README.md index be54c3b..2db4a92 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,15 @@ For more info about Sonobe, check out [Sonobe's docs](https://privacy-scaling-ex ### Usage -### sha_chain.rs (arkworks circuit) +### poseidon_chain.rs (arkworks circuit) +Proves a chain of Poseidon hashes, using the [arkworks/poseidon](https://github.com/arkworks-rs/crypto-primitives/blob/main/crypto-primitives/src/sponge/poseidon/constraints.rs) circuit, with [Nova](https://eprint.iacr.org/2021/370.pdf)+[CycleFold](https://eprint.iacr.org/2023/1192.pdf). + +- `cargo test --release poseidon_chain -- --nocapture` + +### sha_chain_offchain.rs (arkworks circuit) Proves a chain of SHA256 hashes, using the [arkworks/sha256](https://github.com/arkworks-rs/crypto-primitives/blob/main/crypto-primitives/src/crh/sha256/constraints.rs) circuit, with [Nova](https://eprint.iacr.org/2021/370.pdf)+[CycleFold](https://eprint.iacr.org/2023/1192.pdf). -- `cargo test --release sha_chain -- --nocapture` +- `cargo test --release sha_chain_offchain -- --nocapture` ### keccak_chain.rs (circom circuit) Proves a chain of keccak256 hashes, using the [vocdoni/keccak256-circom](https://github.com/vocdoni/keccak256-circom) circuit, with [Nova](https://eprint.iacr.org/2021/370.pdf)+[CycleFold](https://eprint.iacr.org/2023/1192.pdf). @@ -29,11 +34,13 @@ Note: the Circom variant currently has a bit of extra overhead since at each fol ### Repo structure - the Circom circuit (that defines the keccak-chain) to be folded is defined at [./circuit/keccak-chain.circom](https://github.com/arnaucube/hash-chain-sonobe/blob/main/circuit/keccak-chain.circom) -- the logic to fold the circuit using Sonobe is defined at [src/{sha_chain, keccak_chain}.rs](https://github.com/arnaucube/hash-chain-sonobe/blob/main/src) +- the logic to fold the circuit using Sonobe is defined at [src/{poseidon_chain, sha_chain_{offchain, onchain}, keccak_chain}.rs](https://github.com/arnaucube/hash-chain-sonobe/blob/main/src) ## Other -Additionally there is the `src/naive_approach_sha_chain.rs` file, which mimics the amount of hashes computed by the `src/sha_chain.rs` file, but instead of folding it does it by building a big circuit that does all the hashes at once, as we would do before folding existed. +Additionally there is the `src/naive_approach_{poseidon,sha}_chain.rs` file, which mimics the amount of hashes computed by the `src/{poseidon,sha}_chain.rs` file, but instead of folding it does it by building a big circuit that does all the hashes at once, as we would do before folding existed. -To run it: `cargo test --release naive_approach_sha_chain -- --nocapture` +To run it: +- `cargo test --release naive_approach_sha_chain -- --nocapture` +- `cargo test --release naive_approach_poseidon_chain -- --nocapture` diff --git a/src/lib.rs b/src/lib.rs index 127bb53..4247568 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,12 @@ #![allow(non_camel_case_types)] #![allow(clippy::upper_case_acronyms)] -mod keccak_chain; +mod naive_approach_poseidon_chain; mod naive_approach_sha_chain; -mod sha_chain; +mod poseidon_chain; +mod sha_chain_offchain; +mod sha_chain_onchain; mod utils; + +#[cfg(feature = "experimental-frontends")] +mod keccak_chain; diff --git a/src/naive_approach_poseidon_chain.rs b/src/naive_approach_poseidon_chain.rs new file mode 100644 index 0000000..8373401 --- /dev/null +++ b/src/naive_approach_poseidon_chain.rs @@ -0,0 +1,131 @@ +/// This example does the hash chain but in the naive approach: instead of using folding, it does a +/// big circuit containing n instantiations of the Poseidon constraints. + +#[cfg(test)] +mod tests { + use ark_bn254::{Bn254, Fr}; + + use ark_groth16::Groth16; + use ark_snark::SNARK; + + use ark_ff::PrimeField; + + use std::time::Instant; + + use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, + }; + use ark_r1cs_std::fields::fp::FpVar; + use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; + use ark_r1cs_std::{bits::uint8::UInt8, boolean::Boolean, ToBitsGadget, ToBytesGadget}; + use ark_relations::r1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, + }; + + use folding_schemes::transcript::poseidon::poseidon_canonical_config; + + use crate::utils::tests::*; + + /// Test circuit to be folded + #[derive(Clone, Debug)] + pub struct PoseidonChainCircuit { + z_0: Option>, + z_n: Option>, + config: PoseidonConfig, + } + impl ConstraintSynthesizer + for PoseidonChainCircuit + { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + let z_0 = Vec::>::new_witness(cs.clone(), || { + Ok(self.z_0.unwrap_or(vec![F::zero()])) + })?; + let z_n = + Vec::>::new_input(cs.clone(), || Ok(self.z_n.unwrap_or(vec![F::zero()])))?; + + let mut sponge = PoseidonSpongeVar::::new(cs.clone(), &self.config); + let mut z_i: Vec> = z_0.clone(); + for _ in 0..N { + for _ in 0..HASHES_PER_STEP { + sponge.absorb(&z_i)?; + z_i = sponge.squeeze_field_elements(1)?; + } + } + + z_i.enforce_equal(&z_n)?; + Ok(()) + } + } + // compute natively in rust the expected result + fn rust_native_result( + poseidon_config: &PoseidonConfig, + z_0: Vec, + n_steps: usize, + hashes_per_step: usize, + ) -> Vec { + let mut z_i: Vec = z_0.clone(); + for _ in 0..n_steps { + let mut sponge = PoseidonSponge::::new(&poseidon_config); + + for _ in 0..hashes_per_step { + sponge.absorb(&z_i); + z_i = sponge.squeeze_field_elements(1); + } + } + z_i.clone() + } + + #[test] + fn full_flow() { + // set how many iterations of the PoseidonChainCircuit circuit internal loop we want to + // compute + const N_STEPS: usize = 10; + const HASHES_PER_STEP: usize = 400; + println!("running the 'naive' PoseidonChainCircuit, with N_STEPS={}, HASHES_PER_STEP={}. Total hashes = {}", N_STEPS, HASHES_PER_STEP, N_STEPS* HASHES_PER_STEP); + + let poseidon_config = poseidon_canonical_config::(); + + // set the initial state + // let z_0_aux: Vec = vec![0_u32; 32 * 8]; + let z_0_aux: Vec = vec![0_u8; 32]; + let z_0: Vec = z_0_aux.iter().map(|v| Fr::from(*v)).collect::>(); + + // run the N iterations 'natively' in rust to compute the expected `z_n` + let z_n = rust_native_result(&poseidon_config, z_0.clone(), N_STEPS, HASHES_PER_STEP); + + let circuit = PoseidonChainCircuit:: { + z_0: Some(z_0), + z_n: Some(z_n.clone()), + config: poseidon_config, + }; + + let cs = ConstraintSystem::::new_ref(); + circuit.clone().generate_constraints(cs.clone()).unwrap(); + println!( + "number of constraints of the (naive) PoseidonChainCircuit with N_STEPS*HASHES_PER_STEP={} poseidon hashes in total: {} (num constraints)", + N_STEPS * HASHES_PER_STEP, + cs.num_constraints() + ); + + // now let's generate an actual Groth16 proof + let mut rng = rand::rngs::OsRng; + let (g16_pk, g16_vk) = + Groth16::::circuit_specific_setup(circuit.clone(), &mut rng).unwrap(); + + let start = Instant::now(); + let proof = Groth16::::prove(&g16_pk, circuit.clone(), &mut rng).unwrap(); + println!( + "Groth16 proof generation (for the naive PoseidonChainCircuit): {:?}", + start.elapsed() + ); + + let public_inputs = z_n; + let valid_proof = Groth16::::verify(&g16_vk, &public_inputs, &proof).unwrap(); + + assert!(valid_proof); + + println!("finished running the 'naive' PoseidonChainCircuit, with N_STEPS={}, HASHES_PER_STEP={}. Total hashes = {}", N_STEPS, HASHES_PER_STEP, N_STEPS* HASHES_PER_STEP); + } +} diff --git a/src/poseidon_chain.rs b/src/poseidon_chain.rs new file mode 100644 index 0000000..e5e36b7 --- /dev/null +++ b/src/poseidon_chain.rs @@ -0,0 +1,173 @@ +/// +/// This example performs the IVC: +/// - define the circuit to be folded +/// - fold the circuit with Nova+CycleFold's IVC +/// - verify the IVC proof +/// + +#[cfg(test)] +mod tests { + use ark_pallas::{constraints::GVar, Fr, Projective as G1}; + use ark_vesta::{constraints::GVar as GVar2, Projective as G2}; + + use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, + }; + use ark_r1cs_std::fields::fp::FpVar; + + use ark_ff::PrimeField; + use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; + use std::time::Instant; + + use folding_schemes::{ + commitment::pedersen::Pedersen, + folding::nova::{Nova, PreprocessorParam}, + frontend::FCircuit, + transcript::poseidon::poseidon_canonical_config, + Error, FoldingScheme, + }; + + /// Test circuit to be folded + #[derive(Clone, Debug)] + pub struct PoseidonFoldStepCircuit { + config: PoseidonConfig, + } + impl FCircuit + for PoseidonFoldStepCircuit + where + F: Absorb, + { + type Params = PoseidonConfig; + fn new(config: Self::Params) -> Result { + Ok(Self { config }) + } + fn state_len(&self) -> usize { + 1 + } + fn external_inputs_len(&self) -> usize { + 0 + } + fn step_native( + &self, + _i: usize, + z_i: Vec, + _external_inputs: Vec, + ) -> Result, Error> { + let mut sponge = PoseidonSponge::::new(&self.config); + + let mut v = z_i.clone(); + for _ in 0..HASHES_PER_STEP { + sponge.absorb(&v); + v = sponge.squeeze_field_elements(1); + } + Ok(v) + } + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: usize, + z_i: Vec>, + _external_inputs: Vec>, + ) -> Result>, SynthesisError> { + let mut sponge = PoseidonSpongeVar::::new(cs.clone(), &self.config); + + let mut v = z_i.clone(); + for _ in 0..HASHES_PER_STEP { + sponge.absorb(&v)?; + v = sponge.squeeze_field_elements(1)?; + } + Ok(v) + } + } + + #[test] + fn full_flow() { + // set how many steps of folding we want to compute + const N_STEPS: usize = 10; + const HASHES_PER_STEP: usize = 400; + println!("running Nova folding scheme on PoseidonFoldStepCircuit, with N_STEPS={}, HASHES_PER_STEP={}. Total hashes = {}", N_STEPS, HASHES_PER_STEP, N_STEPS* HASHES_PER_STEP); + + // set the initial state + // let z_0_aux: Vec = vec![0_u32; 32 * 8]; + let z_0_aux: Vec = vec![0_u8; 1]; + let z_0: Vec = z_0_aux.iter().map(|v| Fr::from(*v)).collect::>(); + + let poseidon_config = poseidon_canonical_config::(); + let f_circuit = + PoseidonFoldStepCircuit::::new(poseidon_config).unwrap(); + + // ---------------- + // Sanity check + // check that the f_circuit produces valid R1CS constraints + use ark_r1cs_std::alloc::AllocVar; + use ark_r1cs_std::fields::fp::FpVar; + use ark_r1cs_std::R1CSVar; + use ark_relations::r1cs::ConstraintSystem; + let cs = ConstraintSystem::::new_ref(); + let z_0_var = Vec::>::new_witness(cs.clone(), || Ok(z_0.clone())).unwrap(); + let z_1_var = f_circuit + .generate_step_constraints(cs.clone(), 1, z_0_var, vec![]) + .unwrap(); + // check z_1_var against the native z_1 + let z_1_native = f_circuit.step_native(1, z_0.clone(), vec![]).unwrap(); + assert_eq!(z_1_var.value().unwrap(), z_1_native); + // check that the constraint system is satisfied + assert!(cs.is_satisfied().unwrap()); + println!( + "number of constraints of a single instantiation of the PoseidonFoldStepCircuit: {}", + cs.num_constraints() + ); + // ---------------- + + // define type aliases for the FoldingScheme (FS) and Decider (D), to avoid writting the + // whole type each time + pub type FS = Nova< + G1, + GVar, + G2, + GVar2, + PoseidonFoldStepCircuit, + Pedersen, + Pedersen, + false, + >; + + let mut rng = rand::rngs::OsRng; + + // prepare the Nova prover & verifier params + let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); + let start = Instant::now(); + let nova_params = FS::preprocess(&mut rng, &nova_preprocess_params).unwrap(); + println!("Nova params generated: {:?}", start.elapsed()); + + // initialize the folding scheme engine, in our case we use Nova + let mut nova = FS::init(&nova_params, f_circuit, z_0.clone()).unwrap(); + + // run n steps of the folding iteration + let start_full = Instant::now(); + for _ in 0..N_STEPS { + let start = Instant::now(); + nova.prove_step(rng, vec![], None).unwrap(); + println!( + "Nova::prove_step (poseidon) {}: {:?}", + nova.i, + start.elapsed() + ); + } + println!( + "Nova's all {} steps time: {:?}", + N_STEPS, + start_full.elapsed() + ); + + // verify the last IVC proof + let ivc_proof = nova.ivc_proof(); + FS::verify( + nova_params.1.clone(), // Nova's verifier params + ivc_proof, + ) + .unwrap(); + } +} diff --git a/src/sha_chain_offchain.rs b/src/sha_chain_offchain.rs new file mode 100644 index 0000000..cf23750 --- /dev/null +++ b/src/sha_chain_offchain.rs @@ -0,0 +1,182 @@ +/// +/// This example performs the IVC: +/// - define the circuit to be folded +/// - fold the circuit with Nova+CycleFold's IVC +/// - verify the IVC proof +/// + +#[cfg(test)] +mod tests { + use ark_pallas::{constraints::GVar, Fr, Projective as G1}; + use ark_vesta::{constraints::GVar as GVar2, Projective as G2}; + + use ark_crypto_primitives::crh::sha256::{constraints::Sha256Gadget, digest::Digest, Sha256}; + use ark_ff::PrimeField; + use ark_r1cs_std::fields::fp::FpVar; + use ark_r1cs_std::{bits::uint8::UInt8, boolean::Boolean, ToBitsGadget, ToBytesGadget}; + use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; + use std::marker::PhantomData; + use std::time::Instant; + + use folding_schemes::{ + commitment::pedersen::Pedersen, + folding::nova::{Nova, PreprocessorParam}, + frontend::FCircuit, + transcript::poseidon::poseidon_canonical_config, + Error, FoldingScheme, + }; + + use crate::utils::tests::*; + + /// Test circuit to be folded + #[derive(Clone, Copy, Debug)] + pub struct SHA256FoldStepCircuit { + _f: PhantomData, + } + impl FCircuit + for SHA256FoldStepCircuit + { + type Params = (); + fn new(_params: Self::Params) -> Result { + Ok(Self { _f: PhantomData }) + } + fn state_len(&self) -> usize { + 32 + } + fn external_inputs_len(&self) -> usize { + 0 + } + fn step_native( + &self, + _i: usize, + z_i: Vec, + _external_inputs: Vec, + ) -> Result, Error> { + let mut b = f_vec_to_bytes(z_i.to_vec()); + + for _ in 0..HASHES_PER_STEP { + let mut sha256 = Sha256::default(); + sha256.update(b); + b = sha256.finalize().to_vec(); + } + + bytes_to_f_vec(b.to_vec()) // z_{i+1} + } + fn generate_step_constraints( + &self, + _cs: ConstraintSystemRef, + _i: usize, + z_i: Vec>, + _external_inputs: Vec>, + ) -> Result>, SynthesisError> { + let mut b: Vec> = z_i + .iter() + .map(|f| UInt8::::from_bits_le(&f.to_bits_le().unwrap()[..8])) + .collect::>(); + + for _ in 0..HASHES_PER_STEP { + let mut sha256_var = Sha256Gadget::default(); + sha256_var.update(&b).unwrap(); + b = sha256_var.finalize()?.to_bytes()?; + } + + let z_i1: Vec> = b + .iter() + .map(|e| { + let bits = e.to_bits_le().unwrap(); + Boolean::::le_bits_to_fp_var(&bits).unwrap() + }) + .collect(); + + Ok(z_i1) + } + } + + #[test] + fn full_flow() { + // set how many steps of folding we want to compute + const N_STEPS: usize = 5; + const HASHES_PER_STEP: usize = 20; + println!("running Nova folding scheme on SHA256FoldStepCircuit, with N_STEPS={}, HASHES_PER_STEP={}. Total hashes = {}", N_STEPS, HASHES_PER_STEP, N_STEPS* HASHES_PER_STEP); + + // set the initial state + // let z_0_aux: Vec = vec![0_u32; 32 * 8]; + let z_0_aux: Vec = vec![0_u8; 32]; + let z_0: Vec = z_0_aux.iter().map(|v| Fr::from(*v)).collect::>(); + + let f_circuit = SHA256FoldStepCircuit::::new(()).unwrap(); + + // ---------------- + // Sanity check + // check that the f_circuit produces valid R1CS constraints + use ark_r1cs_std::alloc::AllocVar; + use ark_r1cs_std::fields::fp::FpVar; + use ark_r1cs_std::R1CSVar; + use ark_relations::r1cs::ConstraintSystem; + let cs = ConstraintSystem::::new_ref(); + let z_0_var = Vec::>::new_witness(cs.clone(), || Ok(z_0.clone())).unwrap(); + let z_1_var = f_circuit + .generate_step_constraints(cs.clone(), 1, z_0_var, vec![]) + .unwrap(); + // check z_1_var against the native z_1 + let z_1_native = f_circuit.step_native(1, z_0.clone(), vec![]).unwrap(); + assert_eq!(z_1_var.value().unwrap(), z_1_native); + // check that the constraint system is satisfied + assert!(cs.is_satisfied().unwrap()); + println!( + "number of constraints of a single instantiation of the SHA256FoldStepCircuit: {}", + cs.num_constraints() + ); + // ---------------- + + // define type aliases for the FoldingScheme (FS) and Decider (D), to avoid writting the + // whole type each time + pub type FS = Nova< + G1, + GVar, + G2, + GVar2, + SHA256FoldStepCircuit, + Pedersen, + Pedersen, + false, + >; + + let poseidon_config = poseidon_canonical_config::(); + let mut rng = rand::rngs::OsRng; + + // prepare the Nova prover & verifier params + let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit); + let start = Instant::now(); + let nova_params = FS::preprocess(&mut rng, &nova_preprocess_params).unwrap(); + println!("Nova params generated: {:?}", start.elapsed()); + + // initialize the folding scheme engine, in our case we use Nova + let mut nova = FS::init(&nova_params, f_circuit, z_0.clone()).unwrap(); + + // run n steps of the folding iteration + let start_full = Instant::now(); + for _ in 0..N_STEPS { + let start = Instant::now(); + nova.prove_step(rng, vec![], None).unwrap(); + println!( + "Nova::prove_step (sha256) {}: {:?}", + nova.i, + start.elapsed() + ); + } + println!( + "Nova's all {} steps time: {:?}", + N_STEPS, + start_full.elapsed() + ); + + // verify the last IVC proof + let ivc_proof = nova.ivc_proof(); + FS::verify( + nova_params.1.clone(), // Nova's verifier params + ivc_proof, + ) + .unwrap(); + } +} diff --git a/src/sha_chain.rs b/src/sha_chain_onchain.rs similarity index 95% rename from src/sha_chain.rs rename to src/sha_chain_onchain.rs index 3124abd..43c8689 100644 --- a/src/sha_chain.rs +++ b/src/sha_chain_onchain.rs @@ -2,6 +2,7 @@ /// This example performs the full flow: /// - define the circuit to be folded /// - fold the circuit with Nova+CycleFold's IVC +/// - verify the IVC proof /// - generate a DeciderEthCircuit final proof /// - generate the Solidity contract that verifies the proof /// - verify the proof in the EVM @@ -60,8 +61,6 @@ mod tests { fn external_inputs_len(&self) -> usize { 0 } - // function to compute the next state of the folding via rust-native code (not Circom). Used to - // check the Circom values. fn step_native( &self, _i: usize, @@ -176,6 +175,7 @@ mod tests { let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit); let start = Instant::now(); let nova_params = FS::preprocess(&mut rng, &nova_preprocess_params).unwrap(); + let pp_hash = nova_params.1.pp_hash().unwrap(); println!("Nova params generated: {:?}", start.elapsed()); // initialize the folding scheme engine, in our case we use Nova @@ -202,22 +202,17 @@ mod tests { // Sanity check // The following lines contain a sanity check that checks the IVC proof (before going into // the zkSNARK proof) - let (running_instance, incoming_instance, cyclefold_instance) = nova.instances(); + let ivc_proof = nova.ivc_proof(); FS::verify( nova_params.1.clone(), // Nova's verifier params - z_0, - nova.z_i.clone(), - nova.i, - running_instance, - incoming_instance, - cyclefold_instance, + ivc_proof, ) .unwrap(); // ---------------- // prepare the Decider prover & verifier params let start = Instant::now(); - let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + let (decider_pp, decider_vp) = D::preprocess(&mut rng, nova_params, nova.clone()).unwrap(); println!("Decider params generated: {:?}", start.elapsed()); let rng = rand::rngs::OsRng; @@ -244,6 +239,7 @@ mod tests { let calldata: Vec = prepare_calldata( function_selector, + pp_hash, nova.i, nova.z_0, nova.z_i,