add naive_approach_sha_chain.rs to compare folding to naive approach
arnaucube root@arnaucube.com
Wed, 07 Aug 2024 03:35:45 +0200
6 files changed,
183 insertions(+),
19 deletions(-)
M
Cargo.toml
→
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",
M
README.md
→
README.md
@@ -30,3 +30,10 @@
### 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`
M
src/keccak_chain.rs
→
src/keccak_chain.rs
@@ -136,7 +136,11 @@ nova.i,
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();
M
src/lib.rs
→
src/lib.rs
@@ -3,5 +3,6 @@ #![allow(non_camel_case_types)]
#![allow(clippy::upper_case_acronyms)] mod keccak_chain; +mod naive_approach_sha_chain; mod sha_chain; mod utils;
A
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<F: PrimeField, const N: usize, const HASHES_PER_STEP: usize> { + z_0: Option<Vec<F>>, + z_n: Option<Vec<F>>, + } + impl<F: PrimeField, const N: usize, const HASHES_PER_STEP: usize> ConstraintSynthesizer<F> + for SHA256ChainCircuit<F, N, HASHES_PER_STEP> + { + fn generate_constraints(self, cs: ConstraintSystemRef<F>) -> Result<(), SynthesisError> { + let z_0 = Vec::<FpVar<F>>::new_witness(cs.clone(), || { + Ok(self.z_0.unwrap_or(vec![F::zero()])) + })?; + let z_n = + Vec::<FpVar<F>>::new_input(cs.clone(), || Ok(self.z_n.unwrap_or(vec![F::zero()])))?; + let mut z_i: Vec<FpVar<F>> = z_0.clone(); + for _ in 0..N { + let mut b: Vec<UInt8<F>> = z_i + .iter() + .map(|f| UInt8::<F>::from_bits_le(&f.to_bits_le().unwrap()[..8])) + .collect::<Vec<_>>(); + + 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::<F>::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<Fr>, n_steps: usize, hashes_per_step: usize) -> Vec<Fr> { + let mut z_i: Vec<Fr> = 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<u32> = vec![0_u32; 32 * 8]; + let z_0_aux: Vec<u8> = vec![0_u8; 32]; + let z_0: Vec<Fr> = z_0_aux.iter().map(|v| Fr::from(*v)).collect::<Vec<Fr>>(); + + // 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::<Fr, N_STEPS, HASHES_PER_STEP> { + z_0: Some(z_0), + z_n: Some(z_n.clone()), + }; + + let cs = ConstraintSystem::<Fr>::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::<Bn254>::circuit_specific_setup(circuit.clone(), &mut rng).unwrap(); + + let start = Instant::now(); + let proof = Groth16::<Bn254>::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::<Bn254>::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); + } +}
M
src/sha_chain.rs
→
src/sha_chain.rs
@@ -44,10 +44,12 @@ use crate::utils::tests::*;
/// Test circuit to be folded #[derive(Clone, Copy, Debug)] - pub struct SHA256FoldStepCircuit<F: PrimeField> { + pub struct SHA256FoldStepCircuit<F: PrimeField, const HASHES_PER_STEP: usize> { _f: PhantomData<F>, } - impl<F: PrimeField> FCircuit<F> for SHA256FoldStepCircuit<F> { + impl<F: PrimeField, const HASHES_PER_STEP: usize> FCircuit<F> + for SHA256FoldStepCircuit<F, HASHES_PER_STEP> + { type Params = (); fn new(_params: Self::Params) -> Result<Self, Error> { Ok(Self { _f: PhantomData })@@ -66,12 +68,15 @@ _i: usize,
z_i: Vec<F>, _external_inputs: Vec<F>, ) -> Result<Vec<F>, 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 @@ _i: usize,
z_i: Vec<FpVar<F>>, _external_inputs: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> { - let mut sha256_var = Sha256Gadget::default(); - let z_i_u8: Vec<UInt8<F>> = z_i + let mut b: Vec<UInt8<F>> = z_i .iter() .map(|f| UInt8::<F>::from_bits_le(&f.to_bits_le().unwrap()[..8])) .collect::<Vec<_>>(); - sha256_var.update(&z_i_u8).unwrap(); - let z_i1_u8 = sha256_var.finalize()?.to_bytes()?; - let z_i1: Vec<FpVar<F>> = 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<FpVar<F>> = b .iter() .map(|e| { let bits = e.to_bits_le().unwrap();@@ -102,14 +111,16 @@
#[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<u32> = vec![0_u32; 32 * 8]; let z_0_aux: Vec<u8> = vec![0_u8; 32]; let z_0: Vec<Fr> = z_0_aux.iter().map(|v| Fr::from(*v)).collect::<Vec<Fr>>(); - let f_circuit = SHA256FoldStepCircuit::<Fr>::new(()).unwrap(); + let f_circuit = SHA256FoldStepCircuit::<Fr, HASHES_PER_STEP>::new(()).unwrap(); // ---------------- // Sanity check@@ -128,6 +139,10 @@ 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 to avoid writting the whole type each time@@ -136,7 +151,7 @@ G1,
GVar, G2, GVar2, - SHA256FoldStepCircuit<Fr>, + SHA256FoldStepCircuit<Fr, HASHES_PER_STEP>, KZG<'static, Bn254>, Pedersen<G2>, false,@@ -146,7 +161,7 @@ G1,
GVar, G2, GVar2, - SHA256FoldStepCircuit<Fr>, + SHA256FoldStepCircuit<Fr, HASHES_PER_STEP>, KZG<'static, Bn254>, Pedersen<G2>, Groth16<Bn254>,@@ -172,7 +187,7 @@ println!("Decider params generated: {:?}", start.elapsed());
// 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 @@ nova.i,
start.elapsed() ); } - println!("Nova's all steps time: {:?}", start_full.elapsed()); + println!( + "Nova's all {} steps time: {:?}", + N_STEPS, + start_full.elapsed() + ); // ---------------- // Sanity check