diff --git a/Cargo.toml b/Cargo.toml index 8d00e9f..e8ccfdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,6 @@ ff = "0.8" metrics = "=0.13.0-alpha.11" num_cpus = "1.13" rand = "0.7" + +[features] +sanity-checks = [] diff --git a/src/plonk.rs b/src/plonk.rs index ae31166..a3feeed 100644 --- a/src/plonk.rs +++ b/src/plonk.rs @@ -6,11 +6,14 @@ //! [plonk]: https://eprint.iacr.org/2019/953 use crate::arithmetic::CurveAffine; -use crate::poly::{multiopen, Coeff, EvaluationDomain, ExtendedLagrangeCoeff, Polynomial}; +use crate::poly::{ + multiopen, Coeff, EvaluationDomain, ExtendedLagrangeCoeff, LagrangeCoeff, Polynomial, +}; use crate::transcript::ChallengeScalar; mod circuit; mod keygen; +mod lookup; mod permutation; mod prover; mod verifier; @@ -37,6 +40,7 @@ pub struct ProvingKey { vk: VerifyingKey, // TODO: get rid of this? l0: Polynomial, + fixed_values: Vec>, fixed_polys: Vec>, fixed_cosets: Vec>, permutations: Vec>, @@ -49,6 +53,7 @@ pub struct Proof { advice_commitments: Vec, h_commitments: Vec, permutations: Option>, + lookups: Vec>, advice_evals: Vec, aux_evals: Vec, fixed_evals: Vec, @@ -90,6 +95,10 @@ impl VerifyingKey { } } +#[derive(Clone, Copy, Debug)] +struct Theta; +type ChallengeTheta = ChallengeScalar; + #[derive(Clone, Copy, Debug)] struct Beta; type ChallengeBeta = ChallengeScalar; @@ -135,6 +144,8 @@ fn test_proving() { sc: Column, sm: Column, sp: Column, + sl: Column, + sl2: Column, perm: usize, perm2: usize, @@ -151,10 +162,12 @@ fn test_proving() { fn public_input(&mut self, f: F) -> Result where F: FnOnce() -> Result; + fn lookup_table(&mut self, values: &[Vec]) -> Result<(), Error>; } struct MyCircuit { a: Option, + lookup_tables: Vec>, } struct StandardPLONK<'a, F: FieldExt, CS: Assignment + 'a> { @@ -288,6 +301,18 @@ fn test_proving() { Ok(Variable(self.config.a, index)) } + fn lookup_table(&mut self, values: &[Vec]) -> Result<(), Error> { + for (&value_0, &value_1) in values[0].iter().zip(values[1].iter()) { + let index = self.current_gate; + + self.current_gate += 1; + self.cs + .assign_fixed(self.config.sl, index, || Ok(value_0))?; + self.cs + .assign_fixed(self.config.sl2, index, || Ok(value_1))?; + } + Ok(()) + } } impl Circuit for MyCircuit { @@ -310,6 +335,27 @@ fn test_proving() { let sb = meta.fixed_column(); let sc = meta.fixed_column(); let sp = meta.fixed_column(); + let sl = meta.fixed_column(); + let sl2 = meta.fixed_column(); + + /* + * A B ... sl sl2 + * [ + * aux 0 ... 0 0 + * a a ... 0 0 + * a a^2 ... 0 0 + * a a ... 0 0 + * a a^2 ... 0 0 + * ... ... ... ... ... + * ... ... ... aux 0 + * ... ... ... a a + * ... ... ... a a^2 + * ... ... ... 0 0 + * + * ] + */ + meta.lookup(&[a.into()], &[sl.into()]); + meta.lookup(&[a.into(), b.into()], &[sl.into(), sl2.into()]); meta.create_gate(|meta| { let d = meta.query_advice(d, 1); @@ -346,6 +392,8 @@ fn test_proving() { sc, sm, sp, + sl, + sl2, perm, perm2, } @@ -382,22 +430,33 @@ fn test_proving() { cs.copy(b1, c0)?; } + cs.lookup_table(&self.lookup_tables)?; + Ok(()) } } - let circuit: MyCircuit = MyCircuit { - a: Some(Fp::rand()), + let a = Fp::rand(); + let a_squared = a * &a; + let aux = Fp::one() + Fp::one(); + let lookup_table = vec![aux, a, a, Fp::zero()]; + let lookup_table_2 = vec![Fp::zero(), a, a_squared, Fp::zero()]; + + let empty_circuit: MyCircuit = MyCircuit { + a: None, + lookup_tables: vec![lookup_table.clone(), lookup_table_2.clone()], }; - let empty_circuit: MyCircuit = MyCircuit { a: None }; + let circuit: MyCircuit = MyCircuit { + a: Some(a), + lookup_tables: vec![lookup_table, lookup_table_2], + }; // Initialize the proving key let pk = keygen(¶ms, &empty_circuit).expect("keygen should not fail"); let mut pubinputs = pk.get_vk().get_domain().empty_lagrange(); - pubinputs[0] = Fp::one(); - pubinputs[0] += Fp::one(); + pubinputs[0] = aux; let pubinput = params .commit_lagrange(&pubinputs, Blind::default()) .to_affine(); diff --git a/src/plonk/circuit.rs b/src/plonk/circuit.rs index 151ac67..17673bf 100644 --- a/src/plonk/circuit.rs +++ b/src/plonk/circuit.rs @@ -4,7 +4,7 @@ use ff::Field; use std::collections::BTreeMap; use std::convert::TryFrom; -use super::{permutation, Error}; +use super::{lookup, permutation, Error}; use crate::poly::Rotation; /// A column type @@ -313,6 +313,10 @@ pub struct ConstraintSystem { // Vector of permutation arguments, where each corresponds to a sequence of columns // that are involved in a permutation argument. pub(crate) permutations: Vec, + + // Vector of lookup arguments, where each corresponds to a sequence of + // input columns and a sequence of table columns involved in the lookup. + pub(crate) lookups: Vec, } impl Default for ConstraintSystem { @@ -330,6 +334,7 @@ impl Default for ConstraintSystem { aux_queries: Vec::new(), rotations, permutations: Vec::new(), + lookups: Vec::new(), } } } @@ -339,9 +344,7 @@ impl ConstraintSystem { pub fn permutation(&mut self, columns: &[Column]) -> usize { let index = self.permutations.len(); if self.permutations.is_empty() { - let at = Rotation(-1); - let len = self.rotations.len(); - self.rotations.entry(at).or_insert(PointIndex(len)); + self.add_rotation(Rotation(-1)); } for column in columns { @@ -353,12 +356,36 @@ impl ConstraintSystem { index } + /// Add a lookup argument for some input columns and table columns. + /// The function will panic if the number of input columns and table + /// columns are not the same. + pub fn lookup( + &mut self, + input_columns: &[Column], + table_columns: &[Column], + ) -> usize { + assert_eq!(input_columns.len(), table_columns.len()); + + let index = self.lookups.len(); + if self.lookups.is_empty() { + self.add_rotation(Rotation(-1)); + } + + for input in input_columns { + self.query_any_index(*input, 0); + } + for table in table_columns { + self.query_any_index(*table, 0); + } + self.lookups + .push(lookup::Argument::new(input_columns, table_columns)); + + index + } + fn query_fixed_index(&mut self, column: Column, at: i32) -> usize { let at = Rotation(at); - { - let len = self.rotations.len(); - self.rotations.entry(at).or_insert(PointIndex(len)); - } + self.add_rotation(at); // Return existing query, if it exists for (index, fixed_query) in self.fixed_queries.iter().enumerate() { @@ -381,10 +408,7 @@ impl ConstraintSystem { pub(crate) fn query_advice_index(&mut self, column: Column, at: i32) -> usize { let at = Rotation(at); - { - let len = self.rotations.len(); - self.rotations.entry(at).or_insert(PointIndex(len)); - } + self.add_rotation(at); // Return existing query, if it exists for (index, advice_query) in self.advice_queries.iter().enumerate() { @@ -407,10 +431,7 @@ impl ConstraintSystem { fn query_aux_index(&mut self, column: Column, at: i32) -> usize { let at = Rotation(at); - { - let len = self.rotations.len(); - self.rotations.entry(at).or_insert(PointIndex(len)); - } + self.add_rotation(at); // Return existing query, if it exists for (index, aux_query) in self.aux_queries.iter().enumerate() { @@ -432,13 +453,11 @@ impl ConstraintSystem { } fn query_any_index(&mut self, column: Column, at: i32) -> usize { - let index = match column.column_type() { + match column.column_type() { Any::Advice => self.query_advice_index(Column::::try_from(column).unwrap(), at), Any::Fixed => self.query_fixed_index(Column::::try_from(column).unwrap(), at), Any::Aux => self.query_aux_index(Column::::try_from(column).unwrap(), at), - }; - - index + } } /// Query an Any column at a relative position @@ -536,4 +555,9 @@ impl ConstraintSystem { self.num_aux_columns += 1; tmp } + + fn add_rotation(&mut self, at: Rotation) { + let len = self.rotations.len(); + self.rotations.entry(at).or_insert(PointIndex(len)); + } } diff --git a/src/plonk/keygen.rs b/src/plonk/keygen.rs index 1b72625..5df667a 100644 --- a/src/plonk/keygen.rs +++ b/src/plonk/keygen.rs @@ -103,8 +103,8 @@ where let fixed_polys: Vec<_> = assembly .fixed - .into_iter() - .map(|poly| domain.lagrange_to_coeff(poly)) + .iter() + .map(|poly| domain.lagrange_to_coeff(poly.clone())) .collect(); let fixed_cosets = cs @@ -131,6 +131,7 @@ where cs, }, l0, + fixed_values: assembly.fixed, fixed_polys, fixed_cosets, permutations: permutation_pks, diff --git a/src/plonk/lookup.rs b/src/plonk/lookup.rs new file mode 100644 index 0000000..640faef --- /dev/null +++ b/src/plonk/lookup.rs @@ -0,0 +1,33 @@ +use super::circuit::{Any, Column}; +use crate::arithmetic::CurveAffine; + +mod prover; +mod verifier; + +#[derive(Clone, Debug)] +pub(crate) struct Argument { + pub input_columns: Vec>, + pub table_columns: Vec>, +} + +impl Argument { + pub fn new(input_columns: &[Column], table_columns: &[Column]) -> Self { + assert_eq!(input_columns.len(), table_columns.len()); + Argument { + input_columns: input_columns.to_vec(), + table_columns: table_columns.to_vec(), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct Proof { + product_commitment: C, + product_eval: C::Scalar, + product_inv_eval: C::Scalar, + permuted_input_commitment: C, + permuted_table_commitment: C, + permuted_input_eval: C::Scalar, + permuted_input_inv_eval: C::Scalar, + permuted_table_eval: C::Scalar, +} diff --git a/src/plonk/lookup/prover.rs b/src/plonk/lookup/prover.rs new file mode 100644 index 0000000..999db77 --- /dev/null +++ b/src/plonk/lookup/prover.rs @@ -0,0 +1,624 @@ +use super::super::{ + circuit::{Any, Column}, + ChallengeBeta, ChallengeGamma, ChallengeTheta, ChallengeX, Error, ProvingKey, +}; +use super::{Argument, Proof}; +use crate::{ + arithmetic::{eval_polynomial, parallelize, BatchInvert, Curve, CurveAffine, FieldExt}, + poly::{ + commitment::{Blind, Params}, + multiopen::ProverQuery, + Coeff, EvaluationDomain, ExtendedLagrangeCoeff, LagrangeCoeff, Polynomial, Rotation, + }, + transcript::{Hasher, Transcript}, +}; +use ff::Field; +use std::{collections::BTreeMap, iter}; + +#[derive(Debug)] +pub(in crate::plonk) struct Permuted<'a, C: CurveAffine> { + unpermuted_input_columns: Vec<&'a Polynomial>, + unpermuted_input_cosets: Vec<&'a Polynomial>, + permuted_input_column: Polynomial, + permuted_input_poly: Polynomial, + permuted_input_coset: Polynomial, + permuted_input_inv_coset: Polynomial, + permuted_input_blind: Blind, + permuted_input_commitment: C, + unpermuted_table_columns: Vec<&'a Polynomial>, + unpermuted_table_cosets: Vec<&'a Polynomial>, + permuted_table_column: Polynomial, + permuted_table_poly: Polynomial, + permuted_table_coset: Polynomial, + permuted_table_blind: Blind, + permuted_table_commitment: C, +} + +#[derive(Debug)] +pub(in crate::plonk) struct Committed<'a, C: CurveAffine> { + permuted: Permuted<'a, C>, + product_poly: Polynomial, + product_coset: Polynomial, + product_inv_coset: Polynomial, + product_blind: Blind, + product_commitment: C, +} + +pub(in crate::plonk) struct Constructed { + permuted_input_poly: Polynomial, + permuted_input_blind: Blind, + permuted_input_commitment: C, + permuted_table_poly: Polynomial, + permuted_table_blind: Blind, + permuted_table_commitment: C, + product_poly: Polynomial, + product_blind: Blind, + product_commitment: C, +} + +pub(in crate::plonk) struct Evaluated { + constructed: Constructed, + product_eval: C::Scalar, + product_inv_eval: C::Scalar, + permuted_input_eval: C::Scalar, + permuted_input_inv_eval: C::Scalar, + permuted_table_eval: C::Scalar, +} + +impl Argument { + /// Given a Lookup with input columns [A_0, A_1, ..., A_{m-1}] and table columns + /// [S_0, S_1, ..., S_{m-1}], this method + /// - constructs A_compressed = \theta^{m-1} A_0 + theta^{m-2} A_1 + ... + \theta A_{m-2} + A_{m-1} + /// and S_compressed = \theta^{m-1} S_0 + theta^{m-2} S_1 + ... + \theta S_{m-2} + S_{m-1}, + /// - permutes A_compressed and S_compressed using permute_column_pair() helper, + /// obtaining A' and S', and + /// - constructs Permuted struct using permuted_input_value = A', and + /// permuted_table_column = S'. + /// The Permuted struct is used to update the Lookup, and is then returned. + pub(in crate::plonk) fn commit_permuted< + 'a, + C: CurveAffine, + HBase: Hasher, + HScalar: Hasher, + >( + &self, + pk: &ProvingKey, + params: &Params, + domain: &EvaluationDomain, + theta: ChallengeTheta, + advice_values: &'a [Polynomial], + fixed_values: &'a [Polynomial], + aux_values: &'a [Polynomial], + advice_cosets: &'a [Polynomial], + fixed_cosets: &'a [Polynomial], + aux_cosets: &'a [Polynomial], + transcript: &mut Transcript, + ) -> Result, Error> { + // Closure to get values of columns and compress them + let compress_columns = |columns: &[Column]| { + // Values of input columns involved in the lookup + let (unpermuted_columns, unpermuted_cosets): (Vec<_>, Vec<_>) = columns + .iter() + .map(|&column| { + let (values, cosets) = match column.column_type() { + Any::Advice => (advice_values, advice_cosets), + Any::Fixed => (fixed_values, fixed_cosets), + Any::Aux => (aux_values, aux_cosets), + }; + ( + &values[column.index()], + &cosets[pk.vk.cs.get_any_query_index(column, 0)], + ) + }) + .unzip(); + + // Compressed version of columns + let compressed_column = unpermuted_columns + .iter() + .fold(domain.empty_lagrange(), |acc, column| acc * *theta + column); + + (unpermuted_columns, unpermuted_cosets, compressed_column) + }; + + // Closure to construct commitment to column of values + let commit_column = |column: &Polynomial| { + let poly = pk.vk.domain.lagrange_to_coeff(column.clone()); + let blind = Blind(C::Scalar::rand()); + let commitment = params.commit_lagrange(&column, blind).to_affine(); + (poly, blind, commitment) + }; + + // Get values of input columns involved in the lookup and compress them + let (unpermuted_input_columns, unpermuted_input_cosets, compressed_input_column) = + compress_columns(&self.input_columns); + + // Get values of table columns involved in the lookup and compress them + let (unpermuted_table_columns, unpermuted_table_cosets, compressed_table_column) = + compress_columns(&self.table_columns); + + // Permute compressed (InputColumn, TableColumn) pair + let (permuted_input_column, permuted_table_column) = + permute_column_pair::(domain, &compressed_input_column, &compressed_table_column)?; + + // Commit to permuted input column + let (permuted_input_poly, permuted_input_blind, permuted_input_commitment) = + commit_column(&permuted_input_column); + + // Commit to permuted table column + let (permuted_table_poly, permuted_table_blind, permuted_table_commitment) = + commit_column(&permuted_table_column); + + // Hash permuted input commitment + transcript + .absorb_point(&permuted_input_commitment) + .map_err(|_| Error::TranscriptError)?; + + // Hash permuted table commitment + transcript + .absorb_point(&permuted_table_commitment) + .map_err(|_| Error::TranscriptError)?; + + let permuted_input_coset = pk + .vk + .domain + .coeff_to_extended(permuted_input_poly.clone(), Rotation::default()); + let permuted_input_inv_coset = pk + .vk + .domain + .coeff_to_extended(permuted_input_poly.clone(), Rotation(-1)); + let permuted_table_coset = pk + .vk + .domain + .coeff_to_extended(permuted_table_poly.clone(), Rotation::default()); + + Ok(Permuted { + unpermuted_input_columns, + unpermuted_input_cosets, + permuted_input_column, + permuted_input_poly, + permuted_input_coset, + permuted_input_inv_coset, + permuted_input_blind, + permuted_input_commitment, + unpermuted_table_columns, + unpermuted_table_cosets, + permuted_table_column, + permuted_table_poly, + permuted_table_coset, + permuted_table_blind, + permuted_table_commitment, + }) + } +} + +impl<'a, C: CurveAffine> Permuted<'a, C> { + /// Given a Lookup with input columns, table columns, and the permuted + /// input column and permuted table column, this method constructs the + /// grand product polynomial over the lookup. The grand product polynomial + /// is used to populate the Product struct. The Product struct is + /// added to the Lookup and finally returned by the method. + pub(in crate::plonk) fn commit_product, HScalar: Hasher>( + self, + pk: &ProvingKey, + params: &Params, + theta: ChallengeTheta, + beta: ChallengeBeta, + gamma: ChallengeGamma, + transcript: &mut Transcript, + ) -> Result, Error> { + // Goal is to compute the products of fractions + // + // Numerator: (\theta^{m-1} a_0(\omega^i) + \theta^{m-2} a_1(\omega^i) + ... + \theta a_{m-2}(\omega^i) + a_{m-1}(\omega^i) + \beta) + // * (\theta^{m-1} s_0(\omega^i) + \theta^{m-2} s_1(\omega^i) + ... + \theta s_{m-2}(\omega^i) + s_{m-1}(\omega^i) + \gamma) + // Denominator: (a'(\omega^i) + \beta) (s'(\omega^i) + \gamma) + // + // where a_j(X) is the jth input column in this lookup, + // where a'(X) is the compression of the permuted input columns, + // s_j(X) is the jth table column in this lookup, + // s'(X) is the compression of the permuted table columns, + // and i is the ith row of the column. + let mut lookup_product = vec![C::Scalar::zero(); params.n as usize]; + // Denominator uses the permuted input column and permuted table column + parallelize(&mut lookup_product, |lookup_product, start| { + for ((lookup_product, permuted_input_value), permuted_table_value) in lookup_product + .iter_mut() + .zip(self.permuted_input_column[start..].iter()) + .zip(self.permuted_table_column[start..].iter()) + { + *lookup_product = (*beta + permuted_input_value) * &(*gamma + permuted_table_value); + } + }); + + // Batch invert to obtain the denominators for the lookup product + // polynomials + lookup_product.iter_mut().batch_invert(); + + // Finish the computation of the entire fraction by computing the numerators + // (\theta^{m-1} a_0(\omega^i) + \theta^{m-2} a_1(\omega^i) + ... + \theta a_{m-2}(\omega^i) + a_{m-1}(\omega^i) + \beta) + // * (\theta^{m-1} s_0(\omega^i) + \theta^{m-2} s_1(\omega^i) + ... + \theta s_{m-2}(\omega^i) + s_{m-1}(\omega^i) + \gamma) + parallelize(&mut lookup_product, |product, start| { + for (i, product) in product.iter_mut().enumerate() { + let i = i + start; + + // Compress unpermuted input columns + let mut input_term = C::Scalar::zero(); + for unpermuted_input_column in self.unpermuted_input_columns.iter() { + input_term *= θ + input_term += &unpermuted_input_column[i]; + } + + // Compress unpermuted table columns + let mut table_term = C::Scalar::zero(); + for unpermuted_table_column in self.unpermuted_table_columns.iter() { + table_term *= θ + table_term += &unpermuted_table_column[i]; + } + + *product *= &(input_term + &beta); + *product *= &(table_term + &gamma); + } + }); + + // The product vector is a vector of products of fractions of the form + // + // Numerator: (\theta^{m-1} a_0(\omega^i) + \theta^{m-2} a_1(\omega^i) + ... + \theta a_{m-2}(\omega^i) + a_{m-1}(\omega^i) + \beta) + // * (\theta^{m-1} s_0(\omega^i) + \theta^{m-2} s_1(\omega^i) + ... + \theta s_{m-2}(\omega^i) + s_{m-1}(\omega^i) + \gamma) + // Denominator: (a'(\omega^i) + \beta) (s'(\omega^i) + \gamma) + // + // where there are m input columns and m table columns, + // a_j(\omega^i) is the jth input column in this lookup, + // a'j(\omega^i) is the permuted input column, + // s_j(\omega^i) is the jth table column in this lookup, + // s'(\omega^i) is the permuted table column, + // and i is the ith row of the column. + + // Compute the evaluations of the lookup product polynomial + // over our domain, starting with z[0] = 1 + let z = iter::once(C::Scalar::one()) + .chain(lookup_product.into_iter().skip(1)) + .scan(C::Scalar::one(), |state, cur| { + *state *= &cur; + Some(*state) + }) + .collect::>(); + let z = pk.vk.domain.lagrange_from_vec(z); + + #[cfg(feature = "sanity-checks")] + // This test works only with intermediate representations in this method. + // It can be used for debugging purposes. + { + // While in Lagrange basis, check that product is correctly constructed + let n = params.n as usize; + + // z'(X) (a'(X) + \beta) (s'(X) + \gamma) + // - z'(\omega^{-1} X) (\theta^{m-1} a_0(X) + ... + a_{m-1}(X) + \beta) (\theta^{m-1} s_0(X) + ... + s_{m-1}(X) + \gamma) + for i in 0..n { + let prev_idx = (n + i - 1) % n; + + let mut left = z[i]; + let permuted_input_value = &self.permuted_input_column[i]; + + let permuted_table_value = &self.permuted_table_column[i]; + + left *= &(*beta + permuted_input_value); + left *= &(*gamma + permuted_table_value); + + let mut right = z[prev_idx]; + let mut input_term = self.unpermuted_input_columns + .iter() + .fold(C::Scalar::zero(), |acc, input| acc * &theta + &input[i]); + + let mut table_term = self.unpermuted_table_columns + .iter() + .fold(C::Scalar::zero(), |acc, table| acc * &theta + &table[i]); + + input_term += &(*beta); + table_term += &(*gamma); + right *= &(input_term * &table_term); + + assert_eq!(left, right); + } + } + + let product_blind = Blind(C::Scalar::rand()); + let product_commitment = params.commit_lagrange(&z, product_blind).to_affine(); + let z = pk.vk.domain.lagrange_to_coeff(z); + let product_coset = pk + .vk + .domain + .coeff_to_extended(z.clone(), Rotation::default()); + let product_inv_coset = pk.vk.domain.coeff_to_extended(z.clone(), Rotation(-1)); + + // Hash product commitment + transcript + .absorb_point(&product_commitment) + .map_err(|_| Error::TranscriptError)?; + + Ok(Committed::<'a, C> { + permuted: self, + product_poly: z, + product_coset, + product_inv_coset, + product_commitment, + product_blind, + }) + } +} + +impl<'a, C: CurveAffine> Committed<'a, C> { + /// Given a Lookup with input columns, table columns, permuted input + /// column, permuted table column, and grand product polynomial, this + /// method constructs constraints that must hold between these values. + /// This method returns the constraints as a vector of polynomials in + /// the extended evaluation domain. + pub(in crate::plonk) fn construct( + self, + pk: &'a ProvingKey, + theta: ChallengeTheta, + beta: ChallengeBeta, + gamma: ChallengeGamma, + ) -> Result< + ( + Constructed, + impl Iterator> + 'a, + ), + Error, + > { + let permuted = self.permuted; + + let expressions = iter::empty() + // l_0(X) * (1 - z'(X)) = 0 + .chain(Some( + Polynomial::one_minus(self.product_coset.clone()) * &pk.l0, + )) + // z'(X) (a'(X) + \beta) (s'(X) + \gamma) + // - z'(\omega^{-1} X) (\theta^{m-1} a_0(X) + ... + a_{m-1}(X) + \beta) (\theta^{m-1} s_0(X) + ... + s_{m-1}(X) + \gamma) + .chain({ + // z'(X) (a'(X) + \beta) (s'(X) + \gamma) + let mut left = self.product_coset.clone(); + parallelize(&mut left, |left, start| { + for ((left, permuted_input), permuted_table) in left + .iter_mut() + .zip(permuted.permuted_input_coset[start..].iter()) + .zip(permuted.permuted_table_coset[start..].iter()) + { + *left *= &(*permuted_input + &(*beta)); + *left *= &(*permuted_table + &(*gamma)); + } + }); + + // z'(\omega^{-1} X) (\theta^{m-1} a_0(X) + ... + a_{m-1}(X) + \beta) (\theta^{m-1} s_0(X) + ... + s_{m-1}(X) + \gamma) + let mut right = self.product_inv_coset; + parallelize(&mut right, |right, start| { + for (i, right) in right.iter_mut().enumerate() { + let i = i + start; + + // Compress the unpermuted input columns + let mut input_term = C::Scalar::zero(); + for input in permuted.unpermuted_input_cosets.iter() { + input_term *= θ + input_term += &input[i]; + } + + // Compress the unpermuted table columns + let mut table_term = C::Scalar::zero(); + for table in permuted.unpermuted_table_cosets.iter() { + table_term *= θ + table_term += &table[i]; + } + + // Add \beta and \gamma offsets + *right *= &(input_term + &beta); + *right *= &(table_term + &gamma); + } + }); + + Some(left - &right) + }) + // Check that the first values in the permuted input column and permuted + // fixed column are the same. + // l_0(X) * (a'(X) - s'(X)) = 0 + .chain(Some( + (permuted.permuted_input_coset.clone() - &permuted.permuted_table_coset) * &pk.l0, + )) + // Check that each value in the permuted lookup input column is either + // equal to the value above it, or the value at the same index in the + // permuted table column. + // (a′(X)−s′(X))⋅(a′(X)−a′(\omega{-1} X)) = 0 + .chain(Some( + (permuted.permuted_input_coset.clone() - &permuted.permuted_table_coset) + * &(permuted.permuted_input_coset.clone() - &permuted.permuted_input_inv_coset), + )); + + Ok(( + Constructed { + permuted_input_poly: permuted.permuted_input_poly, + permuted_input_blind: permuted.permuted_input_blind, + permuted_input_commitment: permuted.permuted_input_commitment, + permuted_table_poly: permuted.permuted_table_poly, + permuted_table_blind: permuted.permuted_table_blind, + permuted_table_commitment: permuted.permuted_table_commitment, + product_poly: self.product_poly, + product_blind: self.product_blind, + product_commitment: self.product_commitment, + }, + expressions, + )) + } +} + +impl Constructed { + pub(in crate::plonk) fn evaluate, HScalar: Hasher>( + self, + pk: &ProvingKey, + x: ChallengeX, + transcript: &mut Transcript, + ) -> Evaluated { + let domain = &pk.vk.domain; + let x_inv = domain.rotate_omega(*x, Rotation(-1)); + + let product_eval = eval_polynomial(&self.product_poly, *x); + let product_inv_eval = eval_polynomial(&self.product_poly, x_inv); + let permuted_input_eval = eval_polynomial(&self.permuted_input_poly, *x); + let permuted_input_inv_eval = eval_polynomial(&self.permuted_input_poly, x_inv); + let permuted_table_eval = eval_polynomial(&self.permuted_table_poly, *x); + + // Hash each advice evaluation + for eval in iter::empty() + .chain(Some(product_eval)) + .chain(Some(product_inv_eval)) + .chain(Some(permuted_input_eval)) + .chain(Some(permuted_input_inv_eval)) + .chain(Some(permuted_table_eval)) + { + transcript.absorb_scalar(eval); + } + + Evaluated { + constructed: self, + product_eval, + product_inv_eval, + permuted_input_eval, + permuted_input_inv_eval, + permuted_table_eval, + } + } +} + +impl Evaluated { + pub(in crate::plonk) fn open<'a>( + &'a self, + pk: &'a ProvingKey, + x: ChallengeX, + ) -> impl Iterator> + Clone { + let x_inv = pk.vk.domain.rotate_omega(*x, Rotation(-1)); + + iter::empty() + // Open lookup product commitments at x + .chain(Some(ProverQuery { + point: *x, + poly: &self.constructed.product_poly, + blind: self.constructed.product_blind, + eval: self.product_eval, + })) + // Open lookup input commitments at x + .chain(Some(ProverQuery { + point: *x, + poly: &self.constructed.permuted_input_poly, + blind: self.constructed.permuted_input_blind, + eval: self.permuted_input_eval, + })) + // Open lookup table commitments at x + .chain(Some(ProverQuery { + point: *x, + poly: &self.constructed.permuted_table_poly, + blind: self.constructed.permuted_table_blind, + eval: self.permuted_table_eval, + })) + // Open lookup input commitments at x_inv + .chain(Some(ProverQuery { + point: x_inv, + poly: &self.constructed.permuted_input_poly, + blind: self.constructed.permuted_input_blind, + eval: self.permuted_input_eval, + })) + // Open lookup product commitments at x_inv + .chain(Some(ProverQuery { + point: x_inv, + poly: &self.constructed.product_poly, + blind: self.constructed.product_blind, + eval: self.product_eval, + })) + } + + pub(crate) fn build(self) -> Proof { + Proof { + product_commitment: self.constructed.product_commitment, + product_eval: self.product_eval, + product_inv_eval: self.product_inv_eval, + permuted_input_commitment: self.constructed.permuted_input_commitment, + permuted_table_commitment: self.constructed.permuted_table_commitment, + permuted_input_eval: self.permuted_input_eval, + permuted_input_inv_eval: self.permuted_input_inv_eval, + permuted_table_eval: self.permuted_table_eval, + } + } +} + +/// Given a column of input values A and a column of table values S, +/// this method permutes A and S to produce A' and S', such that: +/// - like values in A' are vertically adjacent to each other; and +/// - the first row in a sequence of like values in A' is the row +/// that has the corresponding value in S'. +/// This method returns (A', S') if no errors are encountered. +fn permute_column_pair( + domain: &EvaluationDomain, + input_column: &Polynomial, + table_column: &Polynomial, +) -> Result< + ( + Polynomial, + Polynomial, + ), + Error, +> { + let mut permuted_input_column = input_column.clone(); + + // Sort input lookup column values + permuted_input_column.sort(); + + // A BTreeMap of each unique element in the table column and its count + let mut leftover_table_map: BTreeMap = + table_column.iter().fold(BTreeMap::new(), |mut acc, coeff| { + *acc.entry(*coeff).or_insert(0) += 1; + acc + }); + let mut permuted_table_coeffs = vec![C::Scalar::zero(); table_column.len()]; + + let mut repeated_input_rows = permuted_input_column + .iter() + .zip(permuted_table_coeffs.iter_mut()) + .enumerate() + .filter_map(|(row, (input_value, table_value))| { + // If this is the first occurence of `input_value` in the input column + if row == 0 || *input_value != permuted_input_column[row - 1] { + *table_value = *input_value; + // Remove one instance of input_value from leftover_table_map + if let Some(count) = leftover_table_map.get_mut(&input_value) { + assert!(*count > 0); + *count -= 1; + None + } else { + // Return error if input_value not found + Some(Err(Error::ConstraintSystemFailure)) + } + // If input value is repeated + } else { + Some(Ok(row)) + } + }) + .collect::, _>>()?; + + // Populate permuted table at unfilled rows with leftover table elements + for (coeff, count) in leftover_table_map.iter() { + for _ in 0..*count { + permuted_table_coeffs[repeated_input_rows.pop().unwrap() as usize] = *coeff; + } + } + assert!(repeated_input_rows.is_empty()); + + let mut permuted_table_column = domain.empty_lagrange(); + parallelize( + &mut permuted_table_column, + |permuted_table_column, start| { + for (permuted_table_value, permuted_table_coeff) in permuted_table_column + .iter_mut() + .zip(permuted_table_coeffs[start..].iter()) + { + *permuted_table_value += permuted_table_coeff; + } + }, + ); + + Ok((permuted_input_column, permuted_table_column)) +} diff --git a/src/plonk/lookup/verifier.rs b/src/plonk/lookup/verifier.rs new file mode 100644 index 0000000..d7639e3 --- /dev/null +++ b/src/plonk/lookup/verifier.rs @@ -0,0 +1,149 @@ +use std::iter; + +use super::super::circuit::{Any, Column}; +use super::{Argument, Proof}; +use crate::{ + arithmetic::CurveAffine, + plonk::{ChallengeBeta, ChallengeGamma, ChallengeTheta, ChallengeX, Error, VerifyingKey}, + poly::{multiopen::VerifierQuery, Rotation}, + transcript::{Hasher, Transcript}, +}; +use ff::Field; + +impl Proof { + pub(in crate::plonk) fn absorb_permuted_commitments< + HBase: Hasher, + HScalar: Hasher, + >( + &self, + transcript: &mut Transcript, + ) -> Result<(), Error> { + transcript + .absorb_point(&self.permuted_input_commitment) + .map_err(|_| Error::TranscriptError)?; + transcript + .absorb_point(&self.permuted_table_commitment) + .map_err(|_| Error::TranscriptError) + } + + pub(in crate::plonk) fn absorb_product_commitment< + HBase: Hasher, + HScalar: Hasher, + >( + &self, + transcript: &mut Transcript, + ) -> Result<(), Error> { + transcript + .absorb_point(&self.product_commitment) + .map_err(|_| Error::TranscriptError) + } + + pub(in crate::plonk) fn expressions<'a>( + &'a self, + vk: &'a VerifyingKey, + l_0: C::Scalar, + argument: &'a Argument, + theta: ChallengeTheta, + beta: ChallengeBeta, + gamma: ChallengeGamma, + advice_evals: &[C::Scalar], + fixed_evals: &[C::Scalar], + aux_evals: &[C::Scalar], + ) -> impl Iterator + 'a { + let product_expression = || { + // z'(X) (a'(X) + \beta) (s'(X) + \gamma) + // - z'(\omega^{-1} X) (\theta^{m-1} a_0(X) + ... + a_{m-1}(X) + \beta) (\theta^{m-1} s_0(X) + ... + s_{m-1}(X) + \gamma) + let left = self.product_eval + * &(self.permuted_input_eval + &beta) + * &(self.permuted_table_eval + &gamma); + + let compress_columns = |columns: &[Column]| { + columns + .iter() + .map(|column| { + let index = vk.cs.get_any_query_index(*column, 0); + match column.column_type() { + Any::Advice => advice_evals[index], + Any::Fixed => fixed_evals[index], + Any::Aux => aux_evals[index], + } + }) + .fold(C::Scalar::zero(), |acc, eval| acc * &theta + &eval) + }; + let right = self.product_inv_eval + * &(compress_columns(&argument.input_columns) + &beta) + * &(compress_columns(&argument.table_columns) + &gamma); + + left - &right + }; + + std::iter::empty() + .chain( + // l_0(X) * (1 - z'(X)) = 0 + Some(l_0 * &(C::Scalar::one() - &self.product_eval)), + ) + .chain( + // z'(X) (a'(X) + \beta) (s'(X) + \gamma) + // - z'(\omega^{-1} X) (\theta^{m-1} a_0(X) + ... + a_{m-1}(X) + \beta) (\theta^{m-1} s_0(X) + ... + s_{m-1}(X) + \gamma) + Some(product_expression()), + ) + .chain(Some( + // l_0(X) * (a'(X) - s'(X)) = 0 + l_0 * &(self.permuted_input_eval - &self.permuted_table_eval), + )) + .chain(Some( + // (a′(X)−s′(X))⋅(a′(X)−a′(\omega{-1} X)) = 0 + (self.permuted_input_eval - &self.permuted_table_eval) + * &(self.permuted_input_eval - &self.permuted_input_inv_eval), + )) + } + + pub(in crate::plonk) fn evals(&self) -> impl Iterator { + iter::empty() + .chain(Some(&self.product_eval)) + .chain(Some(&self.product_inv_eval)) + .chain(Some(&self.permuted_input_eval)) + .chain(Some(&self.permuted_input_inv_eval)) + .chain(Some(&self.permuted_table_eval)) + } + + pub(in crate::plonk) fn queries<'a>( + &'a self, + vk: &'a VerifyingKey, + x: ChallengeX, + ) -> impl Iterator> + Clone { + let x_inv = vk.domain.rotate_omega(*x, Rotation(-1)); + + iter::empty() + // Open lookup product commitments at x + .chain(Some(VerifierQuery { + point: *x, + commitment: &self.product_commitment, + eval: self.product_eval, + })) + // Open lookup input commitments at x + .chain(Some(VerifierQuery { + point: *x, + commitment: &self.permuted_input_commitment, + eval: self.permuted_input_eval, + })) + // Open lookup table commitments at x + .chain(Some(VerifierQuery { + point: *x, + commitment: &self.permuted_table_commitment, + eval: self.permuted_table_eval, + })) + // Open lookup input commitments at \omega^{-1} x + .chain(Some(VerifierQuery { + point: x_inv, + commitment: &self.permuted_input_commitment, + eval: self.permuted_input_inv_eval, + })) + // Open lookup product commitments at \omega^{-1} x + .chain(Some(VerifierQuery { + point: x_inv, + commitment: &self.product_commitment, + eval: self.product_inv_eval, + })) + } +} diff --git a/src/plonk/prover.rs b/src/plonk/prover.rs index bce29c1..8b28990 100644 --- a/src/plonk/prover.rs +++ b/src/plonk/prover.rs @@ -3,7 +3,8 @@ use std::iter; use super::{ circuit::{Advice, Assignment, Circuit, Column, ConstraintSystem, Fixed}, - permutation, ChallengeBeta, ChallengeGamma, ChallengeX, ChallengeY, Error, Proof, ProvingKey, + permutation, ChallengeBeta, ChallengeGamma, ChallengeTheta, ChallengeX, ChallengeY, Error, + Proof, ProvingKey, }; use crate::arithmetic::{eval_polynomial, Curve, CurveAffine, FieldExt}; use crate::poly::{ @@ -168,6 +169,32 @@ impl Proof { }) .collect(); + // Sample theta challenge for keeping lookup columns linearly independent + let theta = ChallengeTheta::get(&mut transcript); + + // Construct and commit to permuted values for each lookup + let lookups = pk + .vk + .cs + .lookups + .iter() + .map(|lookup| { + lookup.commit_permuted( + &pk, + ¶ms, + &domain, + theta, + &witness.advice, + &pk.fixed_values, + &aux, + &advice_cosets, + &pk.fixed_cosets, + &aux_cosets, + &mut transcript, + ) + }) + .collect::, _>>()?; + // Sample beta challenge let beta = ChallengeBeta::get(&mut transcript); @@ -188,6 +215,12 @@ impl Proof { None }; + // Construct and commit to products for each lookup + let lookups = lookups + .into_iter() + .map(|lookup| lookup.commit_product(&pk, ¶ms, theta, beta, gamma, &mut transcript)) + .collect::, _>>()?; + // Obtain challenge for keeping all separate gates linearly independent let y = ChallengeY::::get(&mut transcript); @@ -198,6 +231,16 @@ impl Proof { .map(|(p, expressions)| (Some(p), Some(expressions))) .unwrap_or_default(); + // Evaluate the h(X) polynomial's constraint system expressions for the lookup constraints, if any. + let (lookups, lookup_expressions): (Vec<_>, Vec<_>) = { + let tmp = lookups + .into_iter() + .map(|p| p.construct(pk, theta, beta, gamma)) + .collect::, _>>()?; + + tmp.into_iter().unzip() + }; + // Evaluate the h(X) polynomial's constraint system expressions for the constraints provided let h_poly = iter::empty() // Custom constraints @@ -213,6 +256,8 @@ impl Proof { })) // Permutation constraints, if any. .chain(permutation_expressions.into_iter().flatten()) + // Lookup constraints, if any. + .chain(lookup_expressions.into_iter().flatten()) .fold(domain.empty_extended(), |h_poly, v| h_poly * *y + &v); // Divide by t(X) = X^{params.n} - 1. @@ -292,6 +337,12 @@ impl Proof { // Evaluate the permutations, if any, at omega^i x. let permutations = permutations.map(|p| p.evaluate(pk, x, &mut transcript)); + // Evaluate the lookups, if any, at omega^i x. + let lookups = lookups + .into_iter() + .map(|p| p.evaluate(pk, x, &mut transcript)) + .collect::>(); + let instances = iter::empty() .chain(pk.vk.cs.advice_queries.iter().enumerate().map( @@ -335,13 +386,15 @@ impl Proof { let multiopening = multiopen::Proof::create( params, &mut transcript, - instances.chain( - permutations - .as_ref() - .map(|p| p.open(pk, x)) - .into_iter() - .flatten(), - ), + instances + .chain( + permutations + .as_ref() + .map(|p| p.open(pk, x)) + .into_iter() + .flatten(), + ) + .chain(lookups.iter().map(|p| p.open(pk, x)).into_iter().flatten()), ) .map_err(|_| Error::OpeningError)?; @@ -349,6 +402,7 @@ impl Proof { advice_commitments, h_commitments, permutations: permutations.map(|p| p.build()), + lookups: lookups.into_iter().map(|p| p.build()).collect(), advice_evals, fixed_evals, aux_evals, diff --git a/src/plonk/verifier.rs b/src/plonk/verifier.rs index 4925dbd..5db3310 100644 --- a/src/plonk/verifier.rs +++ b/src/plonk/verifier.rs @@ -1,7 +1,10 @@ use ff::Field; use std::iter; -use super::{ChallengeBeta, ChallengeGamma, ChallengeX, ChallengeY, Error, Proof, VerifyingKey}; +use super::{ + ChallengeBeta, ChallengeGamma, ChallengeTheta, ChallengeX, ChallengeY, Error, Proof, + VerifyingKey, +}; use crate::arithmetic::{CurveAffine, FieldExt}; use crate::poly::{ commitment::{Guard, Params, MSM}, @@ -45,6 +48,14 @@ impl<'a, C: CurveAffine> Proof { .map_err(|_| Error::TranscriptError)?; } + // Sample theta challenge for keeping lookup columns linearly independent + let theta = ChallengeTheta::get(&mut transcript); + + // Hash each lookup permuted commitment + for lookup in &self.lookups { + lookup.absorb_permuted_commitments(&mut transcript)?; + } + // Sample beta challenge let beta = ChallengeBeta::get(&mut transcript); @@ -56,6 +67,11 @@ impl<'a, C: CurveAffine> Proof { p.absorb_commitments(&mut transcript)?; } + // Hash each lookup product commitment + for lookup in &self.lookups { + lookup.absorb_product_commitment(&mut transcript)?; + } + // Sample y challenge, which keeps the gates linearly independent. let y = ChallengeY::get(&mut transcript); @@ -72,7 +88,7 @@ impl<'a, C: CurveAffine> Proof { // This check ensures the circuit is satisfied so long as the polynomial // commitments open to the correct values. - self.check_hx(params, vk, beta, gamma, y, x)?; + self.check_hx(params, vk, theta, beta, gamma, y, x)?; for eval in self .advice_evals @@ -87,6 +103,7 @@ impl<'a, C: CurveAffine> Proof { .into_iter() .flatten(), ) + .chain(self.lookups.iter().map(|p| p.evals()).into_iter().flatten()) { transcript.absorb_scalar(*eval); } @@ -136,13 +153,21 @@ impl<'a, C: CurveAffine> Proof { .verify( params, &mut transcript, - queries.chain( - self.permutations - .as_ref() - .map(|p| p.queries(vk, x)) - .into_iter() - .flatten(), - ), + queries + .chain( + self.permutations + .as_ref() + .map(|p| p.queries(vk, x)) + .into_iter() + .flatten(), + ) + .chain( + self.lookups + .iter() + .map(|p| p.queries(vk, x)) + .into_iter() + .flatten(), + ), msm, ) .map_err(|_| Error::OpeningError) @@ -174,6 +199,10 @@ impl<'a, C: CurveAffine> Proof { .map(|p| p.check_lengths(vk)) .transpose()?; + if self.lookups.len() != vk.cs.lookups.len() { + return Err(Error::IncompatibleParams); + } + // TODO: check h_commitments if self.advice_commitments.len() != vk.cs.num_advice_columns { @@ -189,6 +218,7 @@ impl<'a, C: CurveAffine> Proof { &self, params: &'a Params, vk: &VerifyingKey, + theta: ChallengeTheta, beta: ChallengeBeta, gamma: ChallengeGamma, y: ChallengeY, @@ -223,6 +253,26 @@ impl<'a, C: CurveAffine> Proof { .into_iter() .flatten(), ) + .chain( + self.lookups + .iter() + .zip(vk.cs.lookups.iter()) + .map(|(p, argument)| { + p.expressions( + vk, + l_0, + argument, + theta, + beta, + gamma, + &self.advice_evals, + &self.fixed_evals, + &self.aux_evals, + ) + }) + .into_iter() + .flatten(), + ) .fold(C::Scalar::zero(), |h_eval, v| h_eval * &y + &v); // Compute h(x) from the prover