From 559b34b5c5c8916f55cb9fd8ce57656164a18def Mon Sep 17 00:00:00 2001 From: arnaucube Date: Wed, 7 Aug 2024 03:35:45 +0200 Subject: [PATCH] add naive_approach_sha_chain.rs to compare folding to naive approach --- Cargo.toml | 1 + README.md | 7 ++ src/keccak_chain.rs | 6 +- src/lib.rs | 1 + src/naive_approach_sha_chain.rs | 132 ++++++++++++++++++++++++++++++++ src/sha_chain.rs | 55 ++++++++----- 6 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 src/naive_approach_sha_chain.rs diff --git a/Cargo.toml b/Cargo.toml index 277581f..1976467 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ ark-ec = "0.4.1" ark-ff = "0.4.1" ark-r1cs-std = { version = "0.4.0", default-features = false } ark-relations = { version = "0.4.0", default-features = false } +ark-snark = { version = "^0.4.0", default-features = false } ark-poly-commit = "^0.4.0" ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = [ "r1cs", diff --git a/README.md b/README.md index 38c48fc..be54c3b 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,10 @@ 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) + + + +## 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. + +To run it: `cargo test --release naive_approach_sha_chain -- --nocapture` diff --git a/src/keccak_chain.rs b/src/keccak_chain.rs index 20b540a..c062868 100644 --- a/src/keccak_chain.rs +++ b/src/keccak_chain.rs @@ -136,7 +136,11 @@ mod tests { start.elapsed() ); } - println!("Nova's all steps time: {:?}", start_full.elapsed()); + println!( + "Nova's all {} steps time: {:?}", + n_steps, + start_full.elapsed() + ); // perform the hash chain natively in rust (which uses a rust Keccak256 library) let mut z_i_native = z_0.clone(); diff --git a/src/lib.rs b/src/lib.rs index 908318f..127bb53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,6 @@ #![allow(clippy::upper_case_acronyms)] mod keccak_chain; +mod naive_approach_sha_chain; mod sha_chain; mod utils; diff --git a/src/naive_approach_sha_chain.rs b/src/naive_approach_sha_chain.rs new file mode 100644 index 0000000..032963e --- /dev/null +++ b/src/naive_approach_sha_chain.rs @@ -0,0 +1,132 @@ +/// 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 sha256 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::crh::sha256::{constraints::Sha256Gadget, digest::Digest, Sha256}; + 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 crate::utils::tests::*; + + /// Test circuit to be folded + #[derive(Clone, Debug)] + pub struct SHA256ChainCircuit { + z_0: Option>, + z_n: Option>, + } + impl ConstraintSynthesizer + for SHA256ChainCircuit + { + 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 z_i: Vec> = z_0.clone(); + for _ in 0..N { + 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()?; + } + + // update z_i = z_{i+1} + z_i = b + .iter() + .map(|e| { + let bits = e.to_bits_le().unwrap(); + Boolean::::le_bits_to_fp_var(&bits).unwrap() + }) + .collect(); + } + + z_i.enforce_equal(&z_n)?; + Ok(()) + } + } + // compute natively in rust the expected result + fn rust_native_result(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 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(); + } + + z_i = bytes_to_f_vec(b.to_vec()).unwrap(); + } + z_i.clone() + } + + #[test] + fn full_flow() { + // set how many iterations of the SHA256ChainCircuit circuit internal loop we want to + // compute + const N_STEPS: usize = 50; + const HASHES_PER_STEP: usize = 10; + println!("running the 'naive' SHA256ChainCircuit, 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::>(); + + // run the N iterations 'natively' in rust to compute the expected `z_n` + let z_n = rust_native_result(z_0.clone(), N_STEPS, HASHES_PER_STEP); + + let circuit = SHA256ChainCircuit:: { + z_0: Some(z_0), + z_n: Some(z_n.clone()), + }; + + let cs = ConstraintSystem::::new_ref(); + circuit.clone().generate_constraints(cs.clone()).unwrap(); + println!( + "number of constraints of the (naive) SHA256ChainCircuit with N={} hash iterations: {}", + N_STEPS, + 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 SHA256ChainCircuit): {:?}", + 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' SHA256ChainCircuit, with N_STEPS={}, HASHES_PER_STEP={}. Total hashes = {}", N_STEPS, HASHES_PER_STEP, N_STEPS* HASHES_PER_STEP); + } +} diff --git a/src/sha_chain.rs b/src/sha_chain.rs index 4acbb04..c5f559b 100644 --- a/src/sha_chain.rs +++ b/src/sha_chain.rs @@ -44,10 +44,12 @@ mod tests { /// Test circuit to be folded #[derive(Clone, Copy, Debug)] - pub struct SHA256FoldStepCircuit { + pub struct SHA256FoldStepCircuit { _f: PhantomData, } - impl FCircuit for SHA256FoldStepCircuit { + impl FCircuit + for SHA256FoldStepCircuit + { type Params = (); fn new(_params: Self::Params) -> Result { Ok(Self { _f: PhantomData }) @@ -66,12 +68,15 @@ mod tests { z_i: Vec, _external_inputs: Vec, ) -> Result, Error> { - let b = f_vec_to_bytes(z_i.to_vec()); - let mut sha256 = Sha256::default(); - sha256.update(b); - let z_i1 = sha256.finalize().to_vec(); + let mut b = f_vec_to_bytes(z_i.to_vec()); - bytes_to_f_vec(z_i1.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, @@ -80,14 +85,18 @@ mod tests { z_i: Vec>, _external_inputs: Vec>, ) -> Result>, SynthesisError> { - let mut sha256_var = Sha256Gadget::default(); - let z_i_u8: Vec> = z_i + let mut b: Vec> = z_i .iter() .map(|f| UInt8::::from_bits_le(&f.to_bits_le().unwrap()[..8])) .collect::>(); - sha256_var.update(&z_i_u8).unwrap(); - let z_i1_u8 = sha256_var.finalize()?.to_bytes()?; - let z_i1: Vec> = z_i1_u8 + + 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(); @@ -102,14 +111,16 @@ mod tests { #[test] fn full_flow() { // set how many steps of folding we want to compute - let n_steps = 100; + const N_STEPS: usize = 100; + const HASHES_PER_STEP: usize = 10; + 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(); + let f_circuit = SHA256FoldStepCircuit::::new(()).unwrap(); // ---------------- // Sanity check @@ -128,6 +139,10 @@ mod tests { 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 to avoid writting the whole type each time @@ -136,7 +151,7 @@ mod tests { GVar, G2, GVar2, - SHA256FoldStepCircuit, + SHA256FoldStepCircuit, KZG<'static, Bn254>, Pedersen, false, @@ -146,7 +161,7 @@ mod tests { GVar, G2, GVar2, - SHA256FoldStepCircuit, + SHA256FoldStepCircuit, KZG<'static, Bn254>, Pedersen, Groth16, @@ -172,7 +187,7 @@ mod tests { // run n steps of the folding iteration let start_full = Instant::now(); - for _ in 0..n_steps { + for _ in 0..N_STEPS { let start = Instant::now(); nova.prove_step(rng, vec![], None).unwrap(); println!( @@ -181,7 +196,11 @@ mod tests { start.elapsed() ); } - println!("Nova's all steps time: {:?}", start_full.elapsed()); + println!( + "Nova's all {} steps time: {:?}", + N_STEPS, + start_full.elapsed() + ); // ---------------- // Sanity check