Browse Source

add naive_approach_sha_chain.rs to compare folding to naive approach

reduce-memory-usage
arnaucube 3 months ago
parent
commit
559b34b5c5
6 changed files with 183 additions and 19 deletions
  1. +1
    -0
      Cargo.toml
  2. +7
    -0
      README.md
  3. +5
    -1
      src/keccak_chain.rs
  4. +1
    -0
      src/lib.rs
  5. +132
    -0
      src/naive_approach_sha_chain.rs
  6. +37
    -18
      src/sha_chain.rs

+ 1
- 0
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",

+ 7
- 0
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`

+ 5
- 1
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();

+ 1
- 0
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;

+ 132
- 0
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);
}
}

+ 37
- 18
src/sha_chain.rs

@ -44,10 +44,12 @@ mod 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 @@ mod tests {
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 @@ mod tests {
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 @@ 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<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 @@ 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<Fr>,
SHA256FoldStepCircuit<Fr, HASHES_PER_STEP>,
KZG<'static, Bn254>,
Pedersen<G2>,
false,
@ -146,7 +161,7 @@ mod tests {
GVar,
G2,
GVar2,
SHA256FoldStepCircuit<Fr>,
SHA256FoldStepCircuit<Fr, HASHES_PER_STEP>,
KZG<'static, Bn254>,
Pedersen<G2>,
Groth16<Bn254>,
@ -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

Loading…
Cancel
Save