From 7bbc366e5d4e2132d31ef0fa6606c63f50d317ec Mon Sep 17 00:00:00 2001 From: Lef Ioannidis Date: Thu, 22 Apr 2021 13:27:54 -0400 Subject: [PATCH] Arbitrary number of variables and contraints (#34) * This commit makes adding an arbitrary number of variables and inputs possible and removes the implementation leaking to the interface for num_inps + 1 <= num_vars, num_vars: a power of 2, num_cons: a power of 2, but not 1. 1. When creating a new R1CS Instance throught the public interface, it is required # constraints and # of vars be a power of 2. I remove that requirement by padding with dummy constraints and vars until the nearest power of 2. 2. The sumcheck protocol in src/sumcheck.rs does not work for 1 constraint, even though 1 is a power of 2. I have to pad to a minimum of two constraints. 3. Added a test in src/r1csproof.rs called test_padded_constraints. * Move test to src/lib.rs * Remove padding metadata * remove unused use * Simplify padding to power of 2 * run cargo fmt * Fix indexing bug * Rayon is optional, depending on 'multicore' feature * Update rust toolchain * cargo fmt * cleaner to track num_vars_padded and num_cons_padded * cleanup * further cleanup * Cleanup & comments * small fixes * adjust code for padding constraints * fix a bug with pad call * add comment about num_nz_entries * extend padding to NIZK methods extend padding to NIZK methods Co-authored-by: Lef Ioannidis Co-authored-by: Srinath Setty --- .github/workflows/rust.yml | 2 +- Cargo.toml | 4 +- README.md | 7 +- benches/nizk.rs | 4 +- profiler/nizk.rs | 2 +- src/dense_mlpoly.rs | 2 +- src/lib.rs | 256 ++++++++++++++++++++++++++++++++----- 7 files changed, 235 insertions(+), 42 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e811e41..99db8a8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Install - run: rustup default nightly-2021-01-03 + run: rustup default nightly-2021-01-31 - name: Build run: cargo build --verbose - name: Run tests diff --git a/Cargo.toml b/Cargo.toml index 5dcb044..9226ae3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ rand = "0.7.3" digest = "0.8.1" sha3 = "0.8.2" byteorder = "1.3.4" -rayon = "1.3.0" +rayon = { version = "1.3.0", optional = true } serde = { version = "1.0.106", features = ["derive"] } bincode = "1.2.1" subtle = { version = "^2.2.3", default-features = false } @@ -52,5 +52,5 @@ name = "nizk" harness = false [features] -multicore = [] +multicore = ["rayon"] profile = [] diff --git a/README.md b/README.md index d3edaf5..0532604 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Here is another example to use the NIZK variant of the Spartan proof system: let num_inputs = 10; // produce public parameters - let gens = NIZKGens::new(num_cons, num_vars); + let gens = NIZKGens::new(num_cons, num_vars, num_inputs); // ask the library to produce a synthentic R1CS instance let (inst, vars, inputs) = Instance::produce_synthetic_r1cs(num_cons, num_vars, num_inputs); @@ -102,6 +102,7 @@ Here is another example to use the NIZK variant of the Spartan proof system: Finally, we provide an example that specifies a custom R1CS instance instead of using a synthetic instance ```rust +#![allow(non_snake_case)] # extern crate curve25519_dalek; # extern crate libspartan; # extern crate merlin; @@ -163,9 +164,9 @@ Finally, we provide an example that specifies a custom R1CS instance instead of // parameters of the R1CS instance rounded to the nearest power of two let num_cons = 4; - let num_vars = 8; + let num_vars = 5; let num_inputs = 2; - let num_non_zero_entries = 8; + let num_non_zero_entries = 5; // We will encode the above constraints into three matrices, where // the coefficients in the matrix are in the little-endian byte order diff --git a/benches/nizk.rs b/benches/nizk.rs index 42f2634..53d32cc 100644 --- a/benches/nizk.rs +++ b/benches/nizk.rs @@ -24,7 +24,7 @@ fn nizk_prove_benchmark(c: &mut Criterion) { let (inst, vars, inputs) = Instance::produce_synthetic_r1cs(num_cons, num_vars, num_inputs); - let gens = NIZKGens::new(num_cons, num_vars); + let gens = NIZKGens::new(num_cons, num_vars, num_inputs); let name = format!("NIZK_prove_{}", num_vars); group.bench_function(&name, move |b| { @@ -54,7 +54,7 @@ fn nizk_verify_benchmark(c: &mut Criterion) { let num_inputs = 10; let (inst, vars, inputs) = Instance::produce_synthetic_r1cs(num_cons, num_vars, num_inputs); - let gens = NIZKGens::new(num_cons, num_vars); + let gens = NIZKGens::new(num_cons, num_vars, num_inputs); // produce a proof of satisfiability let mut prover_transcript = Transcript::new(b"example"); diff --git a/profiler/nizk.rs b/profiler/nizk.rs index cb3f72d..e184dc9 100644 --- a/profiler/nizk.rs +++ b/profiler/nizk.rs @@ -27,7 +27,7 @@ pub fn main() { let (inst, vars, inputs) = Instance::produce_synthetic_r1cs(num_cons, num_vars, num_inputs); // produce public generators - let gens = NIZKGens::new(num_cons, num_vars); + let gens = NIZKGens::new(num_cons, num_vars, num_inputs); // produce a proof of satisfiability let mut prover_transcript = Transcript::new(b"nizk_example"); diff --git a/src/dense_mlpoly.rs b/src/dense_mlpoly.rs index f802f49..b30e253 100644 --- a/src/dense_mlpoly.rs +++ b/src/dense_mlpoly.rs @@ -149,7 +149,7 @@ impl DensePolynomial { assert_eq!(L_size * R_size, self.Z.len()); let C = (0..L_size) .into_par_iter() - .map(|&i| { + .map(|i| { self.Z[R_size * i..R_size * (i + 1)] .commit(&blinds[i], gens) .compress() diff --git a/src/lib.rs b/src/lib.rs index 55cfdf6..36cae7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,12 @@ extern crate curve25519_dalek; extern crate digest; extern crate merlin; extern crate rand; -extern crate rayon; extern crate sha3; extern crate test; +#[cfg(feature = "multicore")] +extern crate rayon; + mod commitments; mod dense_mlpoly; mod errors; @@ -31,6 +33,7 @@ mod timer; mod transcript; mod unipoly; +use core::cmp::max; use errors::{ProofVerifyError, R1CSError}; use merlin::Transcript; use r1csinstance::{ @@ -86,6 +89,22 @@ impl Assignment { assignment: assignment_scalar.unwrap(), }) } + + /// pads Assignment to the specified length + fn pad(&self, len: usize) -> VarsAssignment { + // check that the new length is higher than current length + assert!(len > self.assignment.len()); + + let padded_assignment = { + let mut padded_assignment = self.assignment.clone(); + padded_assignment.extend(vec![Scalar::zero(); len - self.assignment.len()]); + padded_assignment + }; + + VarsAssignment { + assignment: padded_assignment, + } + } } /// `VarsAssignment` holds an assignment of values to variables in an `Instance` @@ -109,20 +128,37 @@ impl Instance { B: &Vec<(usize, usize, [u8; 32])>, C: &Vec<(usize, usize, [u8; 32])>, ) -> Result { - // check that num_cons is power of 2 - if num_cons.next_power_of_two() != num_cons { - return Err(R1CSError::NonPowerOfTwoCons); - } + let (num_vars_padded, num_cons_padded) = { + let num_vars_padded = { + let mut num_vars_padded = num_vars; - // check that the number of variables is a power of 2 - if num_vars.next_power_of_two() != num_vars { - return Err(R1CSError::NonPowerOfTwoVars); - } + // ensure that num_inputs + 1 <= num_vars + num_vars_padded = max(num_vars_padded, num_inputs + 1); - // check that num_inputs + 1 <= num_vars - if num_inputs >= num_vars { - return Err(R1CSError::InvalidNumberOfInputs); - } + // ensure that num_vars_padded a power of two + if num_vars_padded.next_power_of_two() != num_vars_padded { + num_vars_padded = num_vars_padded.next_power_of_two(); + } + num_vars_padded + }; + + let num_cons_padded = { + let mut num_cons_padded = num_cons; + + // ensure that num_cons_padded is at least 2 + if num_cons_padded == 0 || num_cons_padded == 1 { + num_cons_padded = 2; + } + + // ensure that num_cons_padded is power of 2 + if num_cons.next_power_of_two() != num_cons { + num_cons_padded = num_cons.next_power_of_two(); + } + num_cons_padded + }; + + (num_vars_padded, num_cons_padded) + }; let bytes_to_scalar = |tups: &Vec<(usize, usize, [u8; 32])>| -> Result, R1CSError> { @@ -142,11 +178,26 @@ impl Instance { let val = Scalar::from_bytes(&val_bytes); if val.is_some().unwrap_u8() == 1 { - mat.push((row, col, val.unwrap())); + // if col >= num_vars, it means that it is referencing a 1 or input in the satisfying + // assignment + if col >= num_vars { + mat.push((row, col + num_vars_padded - num_vars, val.unwrap())); + } else { + mat.push((row, col, val.unwrap())); + } } else { return Err(R1CSError::InvalidScalar); } } + + // pad with additional constraints up until num_cons_padded if the original constraints were 0 or 1 + // we do not need to pad otherwise because the dummy constraints are implicit in the sum-check protocol + if num_cons == 0 || num_cons == 1 { + for i in tups.len()..num_cons_padded { + mat.push((i, num_vars, Scalar::zero())); + } + } + Ok(mat) }; @@ -166,8 +217,8 @@ impl Instance { } let inst = R1CSInstance::new( - num_cons, - num_vars, + num_cons_padded, + num_vars_padded, num_inputs, &A_scalar.unwrap(), &B_scalar.unwrap(), @@ -183,15 +234,31 @@ impl Instance { vars: &VarsAssignment, inputs: &InputsAssignment, ) -> Result { - if vars.assignment.len() != self.inst.get_num_vars() { - return Err(R1CSError::InvalidNumberOfVars); + if vars.assignment.len() > self.inst.get_num_vars() { + return Err(R1CSError::InvalidNumberOfInputs); } if inputs.assignment.len() != self.inst.get_num_inputs() { return Err(R1CSError::InvalidNumberOfInputs); } - Ok(self.inst.is_sat(&vars.assignment, &inputs.assignment)) + // we might need to pad variables + let padded_vars = { + let num_padded_vars = self.inst.get_num_vars(); + let num_vars = vars.assignment.len(); + let padded_vars = if num_padded_vars > num_vars { + vars.pad(num_padded_vars) + } else { + vars.clone() + }; + padded_vars + }; + + Ok( + self + .inst + .is_sat(&padded_vars.assignment, &inputs.assignment), + ) } /// Constructs a new synthetic R1CS `Instance` and an associated satisfying assignment @@ -217,12 +284,21 @@ pub struct SNARKGens { impl SNARKGens { /// Constructs a new `SNARKGens` given the size of the R1CS statement + /// `num_nz_entries` specifies the maximum number of non-zero entries in any of the three R1CS matrices pub fn new(num_cons: usize, num_vars: usize, num_inputs: usize, num_nz_entries: usize) -> Self { - let gens_r1cs_sat = R1CSGens::new(b"gens_r1cs_sat", num_cons, num_vars); + let num_vars_padded = { + let mut num_vars_padded = max(num_vars, num_inputs + 1); + if num_vars_padded != num_vars_padded.next_power_of_two() { + num_vars_padded = num_vars_padded.next_power_of_two(); + } + num_vars_padded + }; + + let gens_r1cs_sat = R1CSGens::new(b"gens_r1cs_sat", num_cons, num_vars_padded); let gens_r1cs_eval = R1CSCommitmentGens::new( b"gens_r1cs_eval", num_cons, - num_vars, + num_vars_padded, num_inputs, num_nz_entries, ); @@ -276,14 +352,29 @@ impl SNARK { let mut random_tape = RandomTape::new(b"proof"); transcript.append_protocol_name(SNARK::protocol_name()); let (r1cs_sat_proof, rx, ry) = { - let (proof, rx, ry) = R1CSProof::prove( - &inst.inst, - vars.assignment, - &inputs.assignment, - &gens.gens_r1cs_sat, - transcript, - &mut random_tape, - ); + let (proof, rx, ry) = { + // we might need to pad variables + let padded_vars = { + let num_padded_vars = inst.inst.get_num_vars(); + let num_vars = vars.assignment.len(); + let padded_vars = if num_padded_vars > num_vars { + vars.pad(num_padded_vars) + } else { + vars + }; + padded_vars + }; + + R1CSProof::prove( + &inst.inst, + padded_vars.assignment, + &inputs.assignment, + &gens.gens_r1cs_sat, + transcript, + &mut random_tape, + ) + }; + let proof_encoded: Vec = bincode::serialize(&proof).unwrap(); Timer::print(&format!("len_r1cs_sat_proof {:?}", proof_encoded.len())); @@ -378,8 +469,16 @@ pub struct NIZKGens { impl NIZKGens { /// Constructs a new `NIZKGens` given the size of the R1CS statement - pub fn new(num_cons: usize, num_vars: usize) -> Self { - let gens_r1cs_sat = R1CSGens::new(b"gens_r1cs_sat", num_cons, num_vars); + pub fn new(num_cons: usize, num_vars: usize, num_inputs: usize) -> Self { + let num_vars_padded = { + let mut num_vars_padded = max(num_vars, num_inputs + 1); + if num_vars_padded != num_vars_padded.next_power_of_two() { + num_vars_padded = num_vars_padded.next_power_of_two(); + } + num_vars_padded + }; + + let gens_r1cs_sat = R1CSGens::new(b"gens_r1cs_sat", num_cons, num_vars_padded); NIZKGens { gens_r1cs_sat } } } @@ -410,9 +509,21 @@ impl NIZK { let mut random_tape = RandomTape::new(b"proof"); transcript.append_protocol_name(NIZK::protocol_name()); let (r1cs_sat_proof, rx, ry) = { + // we might need to pad variables + let padded_vars = { + let num_padded_vars = inst.inst.get_num_vars(); + let num_vars = vars.assignment.len(); + let padded_vars = if num_padded_vars > num_vars { + vars.pad(num_padded_vars) + } else { + vars + }; + padded_vars + }; + let (proof, rx, ry) = R1CSProof::prove( &inst.inst, - vars.assignment, + padded_vars.assignment, &input.assignment, &gens.gens_r1cs_sat, transcript, @@ -544,4 +655,85 @@ mod tests { assert_eq!(inst.is_err(), true); assert_eq!(inst.err(), Some(R1CSError::InvalidScalar)); } + + #[test] + fn test_padded_constraints() { + // parameters of the R1CS instance + let num_cons = 1; + let num_vars = 0; + let num_inputs = 3; + let num_non_zero_entries = 3; + + // We will encode the above constraints into three matrices, where + // the coefficients in the matrix are in the little-endian byte order + let mut A: Vec<(usize, usize, [u8; 32])> = Vec::new(); + let mut B: Vec<(usize, usize, [u8; 32])> = Vec::new(); + let mut C: Vec<(usize, usize, [u8; 32])> = Vec::new(); + + // Create a^2 + b + 13 + A.push((0, num_vars + 2, Scalar::one().to_bytes())); // 1*a + B.push((0, num_vars + 2, Scalar::one().to_bytes())); // 1*a + C.push((0, num_vars + 1, Scalar::one().to_bytes())); // 1*z + C.push((0, num_vars, (-Scalar::from(13u64)).to_bytes())); // -13*1 + C.push((0, num_vars + 3, (-Scalar::one()).to_bytes())); // -1*b + + // Var Assignments (Z_0 = 16 is the only output) + let vars = vec![Scalar::zero().to_bytes(); num_vars]; + + // create an InputsAssignment (a = 1, b = 2) + let mut inputs = vec![Scalar::zero().to_bytes(); num_inputs]; + inputs[0] = Scalar::from(16u64).to_bytes(); + inputs[1] = Scalar::from(1u64).to_bytes(); + inputs[2] = Scalar::from(2u64).to_bytes(); + + let assignment_inputs = InputsAssignment::new(&inputs).unwrap(); + let assignment_vars = VarsAssignment::new(&vars).unwrap(); + + // Check if instance is satisfiable + let inst = Instance::new(num_cons, num_vars, num_inputs, &A, &B, &C).unwrap(); + let res = inst.is_sat(&assignment_vars, &assignment_inputs); + assert_eq!(res.unwrap(), true, "should be satisfied"); + + // SNARK public params + let gens = SNARKGens::new(num_cons, num_vars, num_inputs, num_non_zero_entries); + + // create a commitment to the R1CS instance + let (comm, decomm) = SNARK::encode(&inst, &gens); + + // produce a SNARK + let mut prover_transcript = Transcript::new(b"snark_example"); + let proof = SNARK::prove( + &inst, + &decomm, + assignment_vars.clone(), + &assignment_inputs, + &gens, + &mut prover_transcript, + ); + + // verify the SNARK + let mut verifier_transcript = Transcript::new(b"snark_example"); + assert!(proof + .verify(&comm, &assignment_inputs, &mut verifier_transcript, &gens) + .is_ok()); + + // NIZK public params + let gens = NIZKGens::new(num_cons, num_vars, num_inputs); + + // produce a NIZK + let mut prover_transcript = Transcript::new(b"nizk_example"); + let proof = NIZK::prove( + &inst, + assignment_vars, + &assignment_inputs, + &gens, + &mut prover_transcript, + ); + + // verify the NIZK + let mut verifier_transcript = Transcript::new(b"nizk_example"); + assert!(proof + .verify(&inst, &assignment_inputs, &mut verifier_transcript, &gens) + .is_ok()); + } }