use ironfish_bellperson::gadgets::num::AllocatedNum; use ironfish_bellperson::groth16::{ aggregate::{ aggregate_proofs, setup_fake_srs, verify_aggregate_proof, AggregateProof, AggregateVersion, GenericSRS, }, create_random_proof, generate_random_parameters, prepare_verifying_key, verify_proof, verify_proofs_batch, Parameters, Proof, }; use ironfish_bellperson::{Circuit, ConstraintSystem, SynthesisError}; use blstrs::{Bls12, Scalar as Fr}; use ff::{Field, PrimeField}; use group::{Curve, Group}; use itertools::Itertools; use pairing::Engine; use rand::{RngCore, SeedableRng}; use rayon::prelude::*; use serde::Serialize; use std::default::Default; use std::ops::MulAssign; use std::time::{Duration, Instant}; const MIMC_ROUNDS: usize = 322; /// This is an implementation of MiMC, specifically a /// variant named `LongsightF322p3` for BLS12-381. /// See http://eprint.iacr.org/2016/492 for more /// information about this construction. /// /// ``` /// function LongsightF322p3(xL ⦂ Fp, xR ⦂ Fp) { /// for i from 0 up to 321 { /// xL, xR := xR + (xL + Ci)^3, xL /// } /// return xL /// } /// ``` fn mimc(mut xl: Scalar, mut xr: Scalar, constants: &[Scalar]) -> Scalar { assert_eq!(constants.len(), MIMC_ROUNDS); for constant in constants { let mut tmp1 = xl; tmp1.add_assign(constant); let mut tmp2 = tmp1; tmp2 = tmp2.square(); tmp2.mul_assign(&tmp1); tmp2.add_assign(&xr); xr = xl; xl = tmp2; } xl } #[derive(Clone)] struct MimcDemo<'a, Scalar: PrimeField> { xl: Option, xr: Option, constants: &'a [Scalar], } impl<'a, Scalar: PrimeField> Circuit for MimcDemo<'a, Scalar> { fn synthesize>(self, cs: &mut CS) -> Result<(), SynthesisError> { assert_eq!(self.constants.len(), MIMC_ROUNDS); // Allocate the first component of the preimage. let mut xl_value = self.xl; let mut xl = cs.alloc( || "preimage xl", || xl_value.ok_or(SynthesisError::AssignmentMissing), )?; // Allocate the second component of the preimage. let mut xr_value = self.xr; let mut xr = cs.alloc( || "preimage xr", || xr_value.ok_or(SynthesisError::AssignmentMissing), )?; for i in 0..MIMC_ROUNDS { // xL, xR := xR + (xL + Ci)^3, xL let cs = &mut cs.namespace(|| format!("round {}", i)); // tmp = (xL + Ci)^2 let tmp_value = xl_value.map(|mut e| { e.add_assign(&self.constants[i]); e.square() }); let tmp = cs.alloc( || "tmp", || tmp_value.ok_or(SynthesisError::AssignmentMissing), )?; cs.enforce( || "tmp = (xL + Ci)^2", |lc| lc + xl + (self.constants[i], CS::one()), |lc| lc + xl + (self.constants[i], CS::one()), |lc| lc + tmp, ); // new_xL = xR + (xL + Ci)^3 // new_xL = xR + tmp * (xL + Ci) // new_xL - xR = tmp * (xL + Ci) let new_xl_value = xl_value.map(|mut e| { e.add_assign(&self.constants[i]); e.mul_assign(&tmp_value.unwrap()); e.add_assign(&xr_value.unwrap()); e }); let new_xl = if i == (MIMC_ROUNDS - 1) { // This is the last round, xL is our image and so // we allocate a public input. cs.alloc_input( || "image", || new_xl_value.ok_or(SynthesisError::AssignmentMissing), )? } else { cs.alloc( || "new_xl", || new_xl_value.ok_or(SynthesisError::AssignmentMissing), )? }; cs.enforce( || "new_xL = xR + (xL + Ci)^3", |lc| lc + tmp, |lc| lc + xl + (self.constants[i], CS::one()), |lc| lc + new_xl - xr, ); // xR = xL xr = xl; xr_value = xl_value; // xL = new_xL xl = new_xl; xl_value = new_xl_value; } Ok(()) } } #[derive(Clone)] struct TestCircuit { public_inputs: Vec>, witness_input: Option, public_product: Option, } impl Circuit for TestCircuit { fn synthesize>( self, mut cs: &mut CS, ) -> Result<(), SynthesisError> { let input_variables: Vec<_> = self .public_inputs .iter() .enumerate() .map(|(_i, input)| -> Result, SynthesisError> { let num = AllocatedNum::alloc(&mut cs, || { input.ok_or(SynthesisError::AssignmentMissing) })?; num.inputize(&mut cs)?; Ok(num) }) .collect::>()?; let product = AllocatedNum::alloc(&mut cs, || { self.public_product.ok_or(SynthesisError::AssignmentMissing) })?; product.inputize(&mut cs)?; let witness = AllocatedNum::alloc(&mut cs, || { self.witness_input.ok_or(SynthesisError::AssignmentMissing) })?; let mut computed_product = witness; for x in &input_variables { computed_product = computed_product.mul(&mut cs, x)?; } cs.enforce( || "product = computed product", |lc| lc + CS::one(), |lc| lc + computed_product.get_variable(), |lc| lc + product.get_variable(), ); Ok(()) } } #[test] fn test_groth16_srs_io() { use memmap2::MmapOptions; use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use tempfile::NamedTempFile; const NUM_PROOFS_TO_AGGREGATE: usize = 16; let mut rng = rand_chacha::ChaChaRng::seed_from_u64(0u64); println!("Creating parameters..."); // Generate parameters for inner product aggregation let srs: GenericSRS = setup_fake_srs(&mut rng, NUM_PROOFS_TO_AGGREGATE); // Write out parameters to a temp file let mut cache_file = NamedTempFile::new().expect("failed to create temp cache file"); srs.write(&mut cache_file).expect("failed to write out srs"); cache_file.flush().expect("failed to flush srs write"); println!("cache file written to"); // Read back parameters from the temp file cache_file .seek(SeekFrom::Start(0)) .expect("failed to rewind tmp file"); let srs2 = GenericSRS::::read(&mut cache_file).expect("failed to read srs from cache file"); // Ensure that the parameters match assert_eq!(srs, srs2); let cache_path = cache_file.into_temp_path(); let mapped_file = File::open(&cache_path).expect("failed to open file"); let mmap = unsafe { MmapOptions::new() .map(&mapped_file) .expect("failed to mmap") }; let max_len = (2 << 19) + 1; let srs3 = GenericSRS::::read_mmap(&mmap, max_len).expect("failed to read srs from cache file"); // Ensure that the parameters match assert_eq!(srs, srs3); // Remove temp file cache_path.close().expect("failed to close temp path"); } // structure to write to CSV file #[derive(Debug, Serialize, Default)] struct Record { nproofs: u32, // number of proofs that have been verified aggregate_create_ms: u32, // time to create the aggregated proof aggregate_verify_ms: u32, // time to verify the aggregate proof batch_verify_ms: u32, // time to verify all proofs via batching of 10 batch_all_ms: u32, // time ot verify all proofs via batching at once aggregate_size_bytes: u32, // size of the aggregated proof batch_size_bytes: u32, // size of the batch of proof } impl Record { pub fn average(records: &[Record]) -> Record { let mut agg: Record = records.iter().fold(Default::default(), |mut acc, r| { acc.nproofs += r.nproofs; acc.aggregate_create_ms += r.aggregate_create_ms; acc.aggregate_verify_ms += r.aggregate_verify_ms; acc.batch_verify_ms += r.batch_verify_ms; acc.batch_all_ms += r.batch_all_ms; acc.aggregate_size_bytes += r.aggregate_size_bytes; acc.batch_size_bytes += r.batch_size_bytes; acc }); let n = records.len() as u32; agg.nproofs /= n; agg.aggregate_create_ms /= n; agg.aggregate_verify_ms /= n; agg.batch_verify_ms /= n; agg.batch_all_ms /= n; agg.aggregate_size_bytes /= n; agg.batch_size_bytes /= n; agg } } #[test] #[ignore] fn test_groth16_bench() { test_groth16_bench_inner(AggregateVersion::V1); test_groth16_bench_inner(AggregateVersion::V2); } fn test_groth16_bench_inner(version: AggregateVersion) { let n_average = 3; // number of times we do the benchmarking to average out results let nb_proofs = vec![8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]; let max = *nb_proofs.last().unwrap(); let public_inputs = 350; // roughly what a prove commit needs let mut rng = rand_chacha::ChaChaRng::seed_from_u64(0u64); // CRS for aggregation let generic = setup_fake_srs(&mut rng, max); // Create parameters for our circuit let params = { let c = TestCircuit:: { public_inputs: vec![Default::default(); public_inputs], public_product: Default::default(), witness_input: Default::default(), }; generate_random_parameters(c, &mut rng).unwrap() }; // verification key for indivdual verification of proof let pvk = prepare_verifying_key(¶ms.vk); let (proofs, statements): (Vec>, Vec>) = (0..max) .map(|_| generate_proof(public_inputs, ¶ms, &mut rng)) .unzip(); let mut writer = csv::Writer::from_path("aggregation.csv").expect("unable to open csv writer"); println!("Generating {} Groth16 proofs...", max); let mut buf = Vec::new(); proofs[0].write(&mut buf).expect("buffer"); let proof_size = buf.len(); let inclusion = vec![1, 2, 3]; for i in nb_proofs { let mut records = Vec::new(); let (pk, vk) = generic.specialize(i); for _ in 0..n_average { println!("Proofs {}", i); // Aggregate proofs using inner product proofs let start = Instant::now(); println!("\t-Aggregation..."); let aggregate_proof = aggregate_proofs::(&pk, &inclusion, &proofs[..i], version) .expect("failed to aggregate proofs"); let prover_time = start.elapsed().as_millis(); println!("\t-Aggregate Verification ..."); let mut buffer = Vec::new(); aggregate_proof.write(&mut buffer).unwrap(); let start = Instant::now(); let deserialized = AggregateProof::::read(std::io::Cursor::new(&buffer)).unwrap(); let result = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements[..i], &deserialized, &inclusion, version, ); assert!(result.unwrap()); let verifier_time = start.elapsed().as_millis(); println!("\t-Batch per 10 packets verification..."); let batches: Vec<_> = proofs .iter() .cloned() .take(i) .zip(statements.iter().cloned().take(i)) .chunks(10) .into_iter() .map(|s| s.collect()) .collect::, Vec)>>>(); let start = Instant::now(); batches.par_iter().for_each(|batch| { let batch_proofs = batch.iter().by_ref().map(|(p, _)| p).collect::>(); let batch_statements = batch .iter() .map(|(_, state)| state.clone()) .collect::>(); let mut rng = rand_chacha::ChaChaRng::seed_from_u64(0u64); assert!( verify_proofs_batch(&pvk, &mut rng, &batch_proofs, &batch_statements).unwrap() ) }); let batch_verifier_time = start.elapsed().as_millis(); println!("\t-Batch all-in verification..."); let proofs_serialized = proofs.iter().take(i).map(|p| { let mut buff = Vec::new(); p.write(&mut buff).unwrap(); buff }); let start = Instant::now(); let proofs: Vec<_> = proofs_serialized .into_iter() .map(|buff| Proof::::read(std::io::Cursor::new(&buff)).unwrap()) .collect::>(); let proofs_ref: Vec<_> = proofs.iter().collect(); assert!(verify_proofs_batch(&pvk, &mut rng, &proofs_ref, &statements[..i]).unwrap()); let batch_all_time = start.elapsed().as_millis(); let agg_size = buffer.len(); records.push(Record { nproofs: i as u32, aggregate_create_ms: prover_time as u32, aggregate_verify_ms: verifier_time as u32, aggregate_size_bytes: agg_size as u32, batch_verify_ms: batch_verifier_time as u32, batch_size_bytes: (proof_size * i) as u32, batch_all_ms: batch_all_time as u32, }); } let average = Record::average(&records); writer .serialize(average) .expect("unable to write result to csv"); } writer.flush().expect("failed to flush"); } fn generate_proof( publics: usize, p: &Parameters, mut rng: &mut R, ) -> (Proof, Vec) { // Generate random inputs to product together let mut public_inputs = Vec::new(); let mut statement = Vec::new(); let mut prod = Fr::one(); for _i in 0..publics { let x = Fr::from(4u64); public_inputs.push(Some(x)); statement.push(x); prod.mul_assign(&x); } let w = Fr::from(3); let mut product: Fr = w; product.mul_assign(&prod); statement.push(product); let c = TestCircuit { public_inputs, public_product: Some(product), witness_input: Some(w), }; (create_random_proof(c, p, &mut rng).unwrap(), statement) } #[test] fn test_groth16_aggregation() { test_groth16_aggregation_inner(AggregateVersion::V1); test_groth16_aggregation_inner(AggregateVersion::V2); } /// This test creates and aggregates some valid proofs, then tries a bunch of /// failing test case scenarios fn test_groth16_aggregation_inner(version: AggregateVersion) { let _ = env_logger::try_init(); const NUM_PUBLIC_INPUTS: usize = 50; //1000; const NUM_PROOFS: usize = 8; //1024; let mut rng = rand_chacha::ChaChaRng::seed_from_u64(0u64); println!("Creating parameters..."); // Generate parameters for inner product aggregation let generic = setup_fake_srs(&mut rng, NUM_PROOFS); let (pk, vk) = generic.specialize(NUM_PROOFS); // Create parameters for our circuit let params = { let c = TestCircuit:: { public_inputs: vec![Default::default(); NUM_PUBLIC_INPUTS], public_product: Default::default(), witness_input: Default::default(), }; generate_random_parameters(c, &mut rng).unwrap() }; // Prepare the verification key (for proof verification) let pvk = prepare_verifying_key(¶ms.vk); println!("Creating proofs..."); // Generate proofs println!("Generating {} Groth16 proofs...", NUM_PROOFS); let mut proofs = Vec::new(); let mut statements = Vec::new(); let mut generation_time = Duration::new(0, 0); let mut individual_verification_time = Duration::new(0, 0); for _ in 0..NUM_PROOFS { // Generate random inputs to product together let mut public_inputs = Vec::new(); let mut statement = Vec::new(); let mut prod = Fr::one(); for _i in 0..NUM_PUBLIC_INPUTS { let x = Fr::from(4u64); public_inputs.push(Some(x)); statement.push(x); prod.mul_assign(&x); } let w = Fr::from(3); let mut product: Fr = w; product.mul_assign(&prod); statement.push(product); let start = Instant::now(); // Create an instance of our circuit (with the // witness) let c = TestCircuit { public_inputs, public_product: Some(product), witness_input: Some(w), }; // Create a groth16 proof with our parameters. let proof = create_random_proof(c, ¶ms, &mut rng).unwrap(); generation_time += start.elapsed(); assert!(verify_proof(&pvk, &proof, &statement).unwrap()); individual_verification_time += start.elapsed(); proofs.push(proof); statements.push(statement); } let to_include = vec![1, 2, 3]; // 1. Valid proofs println!("Aggregating {} Groth16 proofs...", proofs.len()); let mut aggregate_proof = aggregate_proofs::(&pk, &to_include, &proofs, version) .expect("failed to aggregate proofs"); let result = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, version, ) .expect("these proofs should have been valid"); assert!(result); // Invalid transcript inclusion assert!(!verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &[4, 5, 6], version, ) .unwrap()); // 2. Non power of two let err = aggregate_proofs::(&pk, &to_include, &proofs[0..NUM_PROOFS - 1], version) .unwrap_err(); assert!(matches!(err, SynthesisError::NonPowerOfTwo)); // 3. aggregate invalid proof content (random A, B, and C) let old_a = proofs[0].a; proofs[0].a = ::G1::random(&mut rng).to_affine(); let invalid_agg = aggregate_proofs::(&pk, &to_include, &proofs, version) .expect("I should be able to aggregate"); let res = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &invalid_agg, &to_include, version, ) .expect("no synthesis"); assert!(!res); proofs[0].a = old_a; let old_b = proofs[0].b; proofs[0].b = ::G2::random(&mut rng).to_affine(); let invalid_agg = aggregate_proofs::(&pk, &to_include, &proofs, version) .expect("I should be able to aggregate"); let res = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &invalid_agg, &to_include, version, ) .expect("no synthesis"); assert!(!res); proofs[0].b = old_b; let old_c = proofs[0].c; proofs[0].c = ::G1::random(&mut rng).to_affine(); let invalid_agg = aggregate_proofs::(&pk, &to_include, &proofs, version) .expect("I should be able to aggregate"); let res = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &invalid_agg, &to_include, version, ) .expect("no synthesis"); assert!(!res); proofs[0].c = old_c; // 4. verify with invalid aggregate proof // first invalid commitment let old_aggc = aggregate_proof.agg_c; aggregate_proof.agg_c = ::G1::random(&mut rng); let res = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, version, ) .expect("no synthesis"); assert!(!res); aggregate_proof.agg_c = old_aggc; // 5. invalid gipa element let old_finala = aggregate_proof.tmipp.gipa.final_a; aggregate_proof.tmipp.gipa.final_a = ::G1::random(&mut rng).to_affine(); let res = verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, version, ) .expect("no synthesis"); assert!(!res); aggregate_proof.tmipp.gipa.final_a = old_finala; } #[test] fn test_groth16_aggregation_mimc() { test_groth16_aggregation_mimc_inner(AggregateVersion::V1); test_groth16_aggregation_mimc_inner(AggregateVersion::V2); } fn test_groth16_aggregation_mimc_inner(version: AggregateVersion) { const NUM_PROOFS_TO_AGGREGATE: usize = 8; //1024; let mut rng = rand_chacha::ChaChaRng::seed_from_u64(0u64); // Generate the MiMC round constants let constants = (0..MIMC_ROUNDS) .map(|_| ::Fr::random(&mut rng)) .collect::>(); println!("Creating parameters..."); // Create parameters for our circuit let params = { let c = MimcDemo:: { xl: None, xr: None, constants: &constants, }; generate_random_parameters(c, &mut rng).unwrap() }; // Prepare the verification key (for proof verification) let pvk = prepare_verifying_key(¶ms.vk); // Generate parameters for inner product aggregation // first generic SRS then specialized to the correct size let generic = setup_fake_srs(&mut rng, NUM_PROOFS_TO_AGGREGATE); let (pk, vk) = generic.specialize(NUM_PROOFS_TO_AGGREGATE); println!("Creating proofs..."); // Generate proofs println!("Generating {} Groth16 proofs...", NUM_PROOFS_TO_AGGREGATE); let mut proofs = Vec::new(); let mut images = Vec::new(); let mut generation_time = Duration::new(0, 0); let mut individual_verification_time = Duration::new(0, 0); for _ in 0..NUM_PROOFS_TO_AGGREGATE { // Generate a random preimage and compute the image let xl = ::Fr::random(&mut rng); let xr = ::Fr::random(&mut rng); let image = mimc::(xl, xr, &constants); let start = Instant::now(); // Create an instance of our circuit (with the // witness) let c = MimcDemo { xl: Some(xl), xr: Some(xr), constants: &constants, }; // Create a groth16 proof with our parameters. let proof = create_random_proof(c, ¶ms, &mut rng).unwrap(); generation_time += start.elapsed(); assert!(verify_proof(&pvk, &proof, &[image]).unwrap()); individual_verification_time += start.elapsed(); proofs.push(proof); images.push(vec![image]); } let inclusion = vec![1, 2, 3]; // Aggregate proofs using inner product proofs let start = Instant::now(); println!("Aggregating {} Groth16 proofs...", NUM_PROOFS_TO_AGGREGATE); let aggregate_proof = aggregate_proofs::(&pk, &inclusion, &proofs, version) .expect("failed to aggregate proofs"); let prover_time = start.elapsed().as_millis(); println!("Verifying aggregated proof..."); let start = Instant::now(); let result = verify_aggregate_proof( &vk, &pvk, &mut rng, &images, &aggregate_proof, &inclusion, version, ) .unwrap(); let verifier_time = start.elapsed().as_millis(); assert!(result); let start = Instant::now(); let proofs: Vec<_> = proofs.iter().collect(); assert!(verify_proofs_batch(&pvk, &mut rng, &proofs, &images).unwrap()); let batch_verifier_time = start.elapsed().as_millis(); println!("Proof generation time: {} ms", generation_time.as_millis()); println!("Proof aggregation time: {} ms", prover_time); println!("Proof aggregation verification time: {} ms", verifier_time); println!( "Proof individual verification time: {} ms", individual_verification_time.as_millis() ); println!("Proof batch verification time: {} ms", batch_verifier_time); } #[test] fn test_groth16_aggregate_versions() { let _ = env_logger::try_init(); const NUM_PUBLIC_INPUTS: usize = 50; //1000; const NUM_PROOFS: usize = 8; //1024; let mut rng = rand_chacha::ChaChaRng::seed_from_u64(0u64); println!("Creating parameters..."); // Generate parameters for inner product aggregation let generic = setup_fake_srs(&mut rng, NUM_PROOFS); let (pk, vk) = generic.specialize(NUM_PROOFS); // Create parameters for our circuit let params = { let c = TestCircuit:: { public_inputs: vec![Default::default(); NUM_PUBLIC_INPUTS], public_product: Default::default(), witness_input: Default::default(), }; generate_random_parameters(c, &mut rng).unwrap() }; // Prepare the verification key (for proof verification) let pvk = prepare_verifying_key(¶ms.vk); println!("Creating proofs..."); // Generate proofs println!("Generating {} Groth16 proofs...", NUM_PROOFS); let mut proofs = Vec::new(); let mut statements = Vec::new(); let mut generation_time = Duration::new(0, 0); let mut individual_verification_time = Duration::new(0, 0); for _ in 0..NUM_PROOFS { // Generate random inputs to product together let mut public_inputs = Vec::new(); let mut statement = Vec::new(); let mut prod = Fr::one(); for _i in 0..NUM_PUBLIC_INPUTS { let x = Fr::from(4u64); public_inputs.push(Some(x)); statement.push(x); prod.mul_assign(&x); } let w = Fr::from(3); let mut product: Fr = w; product.mul_assign(&prod); statement.push(product); let start = Instant::now(); // Create an instance of our circuit (with the // witness) let c = TestCircuit { public_inputs, public_product: Some(product), witness_input: Some(w), }; // Create a groth16 proof with our parameters. let proof = create_random_proof(c, ¶ms, &mut rng).unwrap(); generation_time += start.elapsed(); assert!(verify_proof(&pvk, &proof, &statement).unwrap()); individual_verification_time += start.elapsed(); proofs.push(proof); statements.push(statement); } let to_include = vec![1, 2, 3]; // 1. Valid proofs println!("Aggregating {} Groth16 proofs...", proofs.len()); let aggregate_proof = aggregate_proofs::(&pk, &to_include, &proofs, AggregateVersion::V1) .expect("failed to aggregate proofs"); assert!(verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, AggregateVersion::V1, ) .expect("these proofs should have been valid")); assert!(!verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, AggregateVersion::V2, ) .expect("these proofs should have been invalid")); let aggregate_proof = aggregate_proofs::(&pk, &to_include, &proofs, AggregateVersion::V2) .expect("failed to aggregate proofs"); assert!(verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, AggregateVersion::V2, ) .expect("these proofs should have been valid")); assert!(!verify_aggregate_proof( &vk, &pvk, &mut rng, &statements, &aggregate_proof, &to_include, AggregateVersion::V1, ) .expect("these proofs should have been invalid")); }