//! This module defines our main mathematical object `VirtualPolynomial`; and //! various functions associated with it. use crate::errors::PolyIOPErrors; use ark_ff::PrimeField; use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; use ark_serialize::{CanonicalSerialize, SerializationError, Write}; use ark_std::{ end_timer, rand::{Rng, RngCore}, start_timer, }; use std::{cmp::max, collections::HashMap, marker::PhantomData, ops::Add, rc::Rc}; #[rustfmt::skip] /// A virtual polynomial is a sum of products of multilinear polynomials; /// where the multilinear polynomials are stored via their multilinear /// extensions: `(coefficient, DenseMultilinearExtension)` /// /// * Number of products n = `polynomial.products.len()`, /// * Number of multiplicands of ith product m_i = /// `polynomial.products[i].1.len()`, /// * Coefficient of ith product c_i = `polynomial.products[i].0` /// /// The resulting polynomial is /// /// $$ \sum_{i=0}^{n} c_i \cdot \prod_{j=0}^{m_i} P_{ij} $$ /// /// Example: /// f = c0 * f0 * f1 * f2 + c1 * f3 * f4 /// where f0 ... f4 are multilinear polynomials /// /// - flattened_ml_extensions stores the multilinear extension representation of /// f0, f1, f2, f3 and f4 /// - products is /// \[ /// (c0, \[0, 1, 2\]), /// (c1, \[3, 4\]) /// \] /// - raw_pointers_lookup_table maps fi to i /// #[derive(Clone, Debug, Default, PartialEq)] pub struct VirtualPolynomial { /// Aux information about the multilinear polynomial pub aux_info: VPAuxInfo, /// list of reference to products (as usize) of multilinear extension pub products: Vec<(F, Vec)>, /// Stores multilinear extensions in which product multiplicand can refer /// to. pub flattened_ml_extensions: Vec>>, /// Pointers to the above poly extensions raw_pointers_lookup_table: HashMap<*const DenseMultilinearExtension, usize>, } #[derive(Clone, Debug, Default, PartialEq, CanonicalSerialize)] /// Auxiliary information about the multilinear polynomial pub struct VPAuxInfo { /// max number of multiplicands in each product pub max_degree: usize, /// number of variables of the polynomial pub num_variables: usize, /// Associated field #[doc(hidden)] pub phantom: PhantomData, } impl Add for &VirtualPolynomial { type Output = VirtualPolynomial; fn add(self, other: &VirtualPolynomial) -> Self::Output { let start = start_timer!(|| "virtual poly add"); let mut res = self.clone(); for products in other.products.iter() { let cur: Vec>> = products .1 .iter() .map(|&x| other.flattened_ml_extensions[x].clone()) .collect(); res.add_mle_list(cur, products.0) .expect("add product failed"); } end_timer!(start); res } } impl VirtualPolynomial { /// Creates an empty virtual polynomial with `num_variables`. pub fn new(num_variables: usize) -> Self { VirtualPolynomial { aux_info: VPAuxInfo { max_degree: 0, num_variables, phantom: PhantomData::default(), }, products: Vec::new(), flattened_ml_extensions: Vec::new(), raw_pointers_lookup_table: HashMap::new(), } } /// Creates an new virtual polynomial from a MLE and its coefficient. pub fn new_from_mle(mle: Rc>, coefficient: F) -> Self { let mle_ptr: *const DenseMultilinearExtension = Rc::as_ptr(&mle); let mut hm = HashMap::new(); hm.insert(mle_ptr, 0); VirtualPolynomial { aux_info: VPAuxInfo { // The max degree is the max degree of any individual variable max_degree: 1, num_variables: mle.num_vars, phantom: PhantomData::default(), }, // here `0` points to the first polynomial of `flattened_ml_extensions` products: vec![(coefficient, vec![0])], flattened_ml_extensions: vec![mle], raw_pointers_lookup_table: hm, } } /// Add a product of list of multilinear extensions to self /// Returns an error if the list is empty, or the MLE has a different /// `num_vars` from self. /// /// The MLEs will be multiplied together, and then multiplied by the scalar /// `coefficient`. pub fn add_mle_list( &mut self, mle_list: impl IntoIterator>>, coefficient: F, ) -> Result<(), PolyIOPErrors> { let mle_list: Vec>> = mle_list.into_iter().collect(); let mut indexed_product = Vec::with_capacity(mle_list.len()); if mle_list.is_empty() { return Err(PolyIOPErrors::InvalidParameters( "input mle_list is empty".to_string(), )); } self.aux_info.max_degree = max(self.aux_info.max_degree, mle_list.len()); for mle in mle_list { if mle.num_vars != self.aux_info.num_variables { return Err(PolyIOPErrors::InvalidParameters(format!( "product has a multiplicand with wrong number of variables {} vs {}", mle.num_vars, self.aux_info.num_variables ))); } let mle_ptr: *const DenseMultilinearExtension = Rc::as_ptr(&mle); if let Some(index) = self.raw_pointers_lookup_table.get(&mle_ptr) { indexed_product.push(*index) } else { let curr_index = self.flattened_ml_extensions.len(); self.flattened_ml_extensions.push(mle.clone()); self.raw_pointers_lookup_table.insert(mle_ptr, curr_index); indexed_product.push(curr_index); } } self.products.push((coefficient, indexed_product)); Ok(()) } /// Multiple the current VirtualPolynomial by an MLE: /// - add the MLE to the MLE list; /// - multiple each product by MLE and its coefficient. /// Returns an error if the MLE has a different `num_vars` from self. pub fn mul_by_mle( &mut self, mle: Rc>, coefficient: F, ) -> Result<(), PolyIOPErrors> { let start = start_timer!(|| "mul by mle"); if mle.num_vars != self.aux_info.num_variables { return Err(PolyIOPErrors::InvalidParameters(format!( "product has a multiplicand with wrong number of variables {} vs {}", mle.num_vars, self.aux_info.num_variables ))); } let mle_ptr: *const DenseMultilinearExtension = Rc::as_ptr(&mle); // check if this mle already exists in the virtual polynomial let mle_index = match self.raw_pointers_lookup_table.get(&mle_ptr) { Some(&p) => p, None => { self.raw_pointers_lookup_table .insert(mle_ptr, self.flattened_ml_extensions.len()); self.flattened_ml_extensions.push(mle); self.flattened_ml_extensions.len() - 1 }, }; for (prod_coef, indices) in self.products.iter_mut() { // - add the MLE to the MLE list; // - multiple each product by MLE and its coefficient. indices.push(mle_index); *prod_coef *= coefficient; } // increase the max degree by one as the MLE has degree 1. self.aux_info.max_degree += 1; end_timer!(start); Ok(()) } /// Evaluate the virtual polynomial at point `point`. /// Returns an error is point.len() does not match `num_variables`. pub fn evaluate(&self, point: &[F]) -> Result { let start = start_timer!(|| "evaluation"); if self.aux_info.num_variables != point.len() { return Err(PolyIOPErrors::InvalidParameters(format!( "wrong number of variables {} vs {}", self.aux_info.num_variables, point.len() ))); } let evals: Vec = self .flattened_ml_extensions .iter() .map(|x| { x.evaluate(point).unwrap() // safe unwrap here since we have // already checked that num_var // matches }) .collect(); let res = self .products .iter() .map(|(c, p)| *c * p.iter().map(|&i| evals[i]).product::()) .sum(); end_timer!(start); Ok(res) } /// Sample a random virtual polynomial, return the polynomial and its sum. pub fn rand( nv: usize, num_multiplicands_range: (usize, usize), num_products: usize, rng: &mut R, ) -> Result<(Self, F), PolyIOPErrors> { let start = start_timer!(|| "sample random virtual polynomial"); let mut sum = F::zero(); let mut poly = VirtualPolynomial::new(nv); for _ in 0..num_products { let num_multiplicands = rng.gen_range(num_multiplicands_range.0..num_multiplicands_range.1); let (product, product_sum) = random_mle_list(nv, num_multiplicands, rng); let coefficient = F::rand(rng); poly.add_mle_list(product.into_iter(), coefficient)?; sum += product_sum * coefficient; } end_timer!(start); Ok((poly, sum)) } /// Sample a random virtual polynomial that evaluates to zero everywhere /// over the boolean hypercube. pub fn rand_zero( nv: usize, num_multiplicands_range: (usize, usize), num_products: usize, rng: &mut R, ) -> Result { let mut poly = VirtualPolynomial::new(nv); for _ in 0..num_products { let num_multiplicands = rng.gen_range(num_multiplicands_range.0..num_multiplicands_range.1); let product = random_zero_mle_list(nv, num_multiplicands, rng); let coefficient = F::rand(rng); poly.add_mle_list(product.into_iter(), coefficient)?; } Ok(poly) } // Input poly f(x) and a random vector r, output // \hat f(x) = \sum_{x_i \in eval_x} f(x_i) eq(x, r) // where // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) // // This function is used in ZeroCheck. pub(crate) fn build_f_hat(&self, r: &[F]) -> Result { let start = start_timer!(|| "zero check build hat f"); if self.aux_info.num_variables != r.len() { return Err(PolyIOPErrors::InvalidParameters(format!( "r.len() is different from number of variables: {} vs {}", r.len(), self.aux_info.num_variables ))); } let eq_x_r = build_eq_x_r(r)?; let mut res = self.clone(); res.mul_by_mle(eq_x_r, F::one())?; end_timer!(start); Ok(res) } } /// Sample a random list of multilinear polynomials. /// Returns /// - the list of polynomials, /// - its sum of polynomial evaluations over the boolean hypercube. fn random_mle_list( nv: usize, degree: usize, rng: &mut R, ) -> (Vec>>, F) { let start = start_timer!(|| "sample random mle list"); let mut multiplicands = Vec::with_capacity(degree); for _ in 0..degree { multiplicands.push(Vec::with_capacity(1 << nv)) } let mut sum = F::zero(); for _ in 0..(1 << nv) { let mut product = F::one(); for e in multiplicands.iter_mut() { let val = F::rand(rng); e.push(val); product *= val; } sum += product; } let list = multiplicands .into_iter() .map(|x| Rc::new(DenseMultilinearExtension::from_evaluations_vec(nv, x))) .collect(); end_timer!(start); (list, sum) } // Build a randomize list of mle-s whose sum is zero. pub fn random_zero_mle_list( nv: usize, degree: usize, rng: &mut R, ) -> Vec>> { let start = start_timer!(|| "sample random zero mle list"); let mut multiplicands = Vec::with_capacity(degree); for _ in 0..degree { multiplicands.push(Vec::with_capacity(1 << nv)) } for _ in 0..(1 << nv) { multiplicands[0].push(F::zero()); for e in multiplicands.iter_mut().skip(1) { e.push(F::rand(rng)); } } let list = multiplicands .into_iter() .map(|x| Rc::new(DenseMultilinearExtension::from_evaluations_vec(nv, x))) .collect(); end_timer!(start); list } // This function build the eq(x, r) polynomial for any given r. // // Evaluate // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) // over r, which is // eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) fn build_eq_x_r(r: &[F]) -> Result>, PolyIOPErrors> { let start = start_timer!(|| "zero check build eq_x_r"); // we build eq(x,r) from its evaluations // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars // for example, with num_vars = 4, x is a binary vector of 4, then // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) // .... // 1 1 1 1 -> r0 * r1 * r2 * r3 // we will need 2^num_var evaluations let mut eval = Vec::new(); build_eq_x_r_helper(r, &mut eval)?; let mle = DenseMultilinearExtension::from_evaluations_vec(r.len(), eval); let res = Rc::new(mle); end_timer!(start); Ok(res) } /// A helper function to build eq(x, r) recursively. /// This function takes `r.len()` steps, and for each step it requires a maximum /// `r.len()-1` multiplications. fn build_eq_x_r_helper(r: &[F], buf: &mut Vec) -> Result<(), PolyIOPErrors> { if r.is_empty() { return Err(PolyIOPErrors::InvalidParameters( "r length is 0".to_string(), )); } else if r.len() == 1 { // initializing the buffer with [1-r_0, r_0] buf.push(F::one() - r[0]); buf.push(r[0]); } else { build_eq_x_r_helper(&r[1..], buf)?; // suppose at the previous step we received [b_1, ..., b_k] // for the current step we will need // if x_0 = 0: (1-r0) * [b_1, ..., b_k] // if x_0 = 1: r0 * [b_1, ..., b_k] let mut res = vec![]; for &b_i in buf.iter() { let tmp = r[0] * b_i; res.push(b_i - tmp); res.push(tmp); } *buf = res; } Ok(()) } #[cfg(test)] mod test { use super::*; use crate::utils::bit_decompose; use ark_bls12_381::Fr; use ark_ff::UniformRand; use ark_std::test_rng; #[test] fn test_virtual_polynomial_additions() -> Result<(), PolyIOPErrors> { let mut rng = test_rng(); for nv in 2..5 { for num_products in 2..5 { let base: Vec = (0..nv).map(|_| Fr::rand(&mut rng)).collect(); let (a, _a_sum) = VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; let (b, _b_sum) = VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; let c = &a + &b; assert_eq!( a.evaluate(base.as_ref())? + b.evaluate(base.as_ref())?, c.evaluate(base.as_ref())? ); } } Ok(()) } #[test] fn test_virtual_polynomial_mul_by_mle() -> Result<(), PolyIOPErrors> { let mut rng = test_rng(); for nv in 2..5 { for num_products in 2..5 { let base: Vec = (0..nv).map(|_| Fr::rand(&mut rng)).collect(); let (a, _a_sum) = VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; let (b, _b_sum) = random_mle_list(nv, 1, &mut rng); let b_mle = b[0].clone(); let coeff = Fr::rand(&mut rng); let b_vp = VirtualPolynomial::new_from_mle(b_mle.clone(), coeff); let mut c = a.clone(); c.mul_by_mle(b_mle, coeff)?; assert_eq!( a.evaluate(base.as_ref())? * b_vp.evaluate(base.as_ref())?, c.evaluate(base.as_ref())? ); } } Ok(()) } #[test] fn test_eq_xr() { let mut rng = test_rng(); for nv in 4..10 { let r: Vec = (0..nv).map(|_| Fr::rand(&mut rng)).collect(); let eq_x_r = build_eq_x_r(r.as_ref()).unwrap(); let eq_x_r2 = build_eq_x_r_for_test(r.as_ref()); assert_eq!(eq_x_r, eq_x_r2); } } /// Naive method to build eq(x, r). /// Only used for testing purpose. // Evaluate // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) // over r, which is // eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) fn build_eq_x_r_for_test(r: &[F]) -> Rc> { let start = start_timer!(|| "zero check naive build eq_x_r"); // we build eq(x,r) from its evaluations // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars // for example, with num_vars = 4, x is a binary vector of 4, then // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) // .... // 1 1 1 1 -> r0 * r1 * r2 * r3 // we will need 2^num_var evaluations // First, we build array for {1 - r_i} let one_minus_r: Vec = r.iter().map(|ri| F::one() - ri).collect(); let num_var = r.len(); let mut eval = vec![]; for i in 0..1 << num_var { let mut current_eval = F::one(); let bit_sequence = bit_decompose(i, num_var); for (&bit, (ri, one_minus_ri)) in bit_sequence.iter().zip(r.iter().zip(one_minus_r.iter())) { current_eval *= if bit { *ri } else { *one_minus_ri }; } eval.push(current_eval); } let mle = DenseMultilinearExtension::from_evaluations_vec(num_var, eval); let res = Rc::new(mle); end_timer!(start); res } }