From c3a99ac3f6bbb41c8f5826980e4cb0853b428e82 Mon Sep 17 00:00:00 2001 From: Tom Shen Date: Sun, 6 Jun 2021 12:56:30 -0700 Subject: [PATCH] Let `Radix2Domain::offset` to be `FpVar` instead of `F` (#65) * restructure code * done * add changelog * add the changelog to mark this as a breaking change * add the CHANGELOG * tweak * add `EqGadget` * rename generate_interpolate_cache to generate_interpolation_cache * address the comment Co-authored-by: weikeng --- CHANGELOG.md | 13 +- src/poly/domain/mod.rs | 26 ++- .../univariate/lagrange_interpolator.rs | 19 +- src/poly/evaluations/univariate/mod.rs | 190 +++++++++++++++--- 4 files changed, 211 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2f82c..057b8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,29 +2,35 @@ ### Breaking changes -- #60 Rename `AllocatedBit` to `AllocatedBool` for consistency with the `Boolean` variable. -You can update downstream usage with `grep -rl 'AllocatedBit' . | xargs env LANG=C env LC_CTYPE=C sed -i '' 's/AllocatedBit/AllocatedBool/g'`. +- [\#60](https://github.com/arkworks-rs/r1cs-std/pull/60) Rename `AllocatedBit` to `AllocatedBool` for consistency with the `Boolean` variable. + You can update downstream usage with `grep -rl 'AllocatedBit' . | xargs env LANG=C env LC_CTYPE=C sed -i '' 's/AllocatedBit/AllocatedBool/g'`. +- [\#65](https://github.com/arkworks-rs/r1cs-std/pull/65) Rename `Radix2Domain` in `r1cs-std` to `Radix2DomainVar`. ### Features -- [\#53](https://github.com/arkworks-rs/r1cs-std/pull/53) Add univariate evaluation domain and lagrange interpolation. +- [\#53](https://github.com/arkworks-rs/r1cs-std/pull/53) Add univariate evaluation domain and Lagrange interpolation. ### Improvements +- [\#65](https://github.com/arkworks-rs/r1cs-std/pull/65) Add support for non-constant coset offset in `Radix2DomainVar`. + ### Bug Fixes ## v0.2.0 ### Breaking changes + - [\#12](https://github.com/arkworks-rs/r1cs-std/pull/12) Make the output of the `ToBitsGadget` impl for `FpVar` fixed-size - [\#48](https://github.com/arkworks-rs/r1cs-std/pull/48) Add `Clone` trait bound to `CondSelectGadget`. ### Features + - [\#21](https://github.com/arkworks-rs/r1cs-std/pull/21) Add `UInt128` - [\#50](https://github.com/arkworks-rs/r1cs-std/pull/50) Add `DensePolynomialVar` ### Improvements + - [\#5](https://github.com/arkworks-rs/r1cs-std/pull/5) Speedup BLS-12 pairing - [\#13](https://github.com/arkworks-rs/r1cs-std/pull/13) Add `ToConstraintFieldGadget` to `ProjectiveVar` - [\#15](https://github.com/arkworks-rs/r1cs-std/pull/15), #16 Allow `cs` to be `None` when converting a Montgomery point into a Twisted Edwards point @@ -38,6 +44,7 @@ You can update downstream usage with `grep -rl 'AllocatedBit' . | xargs env LANG - [\#46](https://github.com/arkworks-rs/r1cs-std/pull/46) Add mux gadget as an auto-impl in `CondSelectGadget` to support random access of an array ### Bug fixes + - [\#8](https://github.com/arkworks-rs/r1cs-std/pull/8) Fix bug in `three_bit_cond_neg_lookup` when using a constant lookup bit - [\#9](https://github.com/arkworks-rs/r1cs-std/pull/9) Fix bug in `short_weierstrass::ProjectiveVar::to_affine` - [\#29](https://github.com/arkworks-rs/r1cs-std/pull/29) Fix `to_non_unique_bytes` for `BLS12::G1Prepared` diff --git a/src/poly/domain/mod.rs b/src/poly/domain/mod.rs index 35e0189..4959682 100644 --- a/src/poly/domain/mod.rs +++ b/src/poly/domain/mod.rs @@ -1,4 +1,5 @@ use crate::boolean::Boolean; +use crate::eq::EqGadget; use crate::fields::fp::FpVar; use crate::fields::FieldVar; use ark_ff::PrimeField; @@ -7,23 +8,33 @@ use ark_std::vec::Vec; pub mod vanishing_poly; -#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +#[derive(Clone, Debug)] /// Defines an evaluation domain over a prime field. The domain is a coset of size `1< { +pub struct Radix2DomainVar { /// generator of subgroup g pub gen: F, /// index of the quotient group (i.e. the `offset`) - pub offset: F, + pub offset: FpVar, /// dimension of evaluation domain pub dim: u64, } -impl Radix2Domain { +impl EqGadget for Radix2DomainVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + if self.gen != other.gen || self.dim != other.dim { + Ok(Boolean::constant(false)) + } else { + self.offset.is_eq(&other.offset) + } + } +} + +impl Radix2DomainVar { /// order of the domain pub fn order(&self) -> usize { 1 << self.dim @@ -40,6 +51,11 @@ impl Radix2Domain { result } + /// Size of the domain + pub fn size(&self) -> u64 { + 1 << self.dim + } + /// For domain `h` with dimension `n`, `position` represented by `query_pos` in big endian form, /// returns `h*g^{position}` pub fn query_position_to_coset( @@ -64,7 +80,7 @@ impl Radix2Domain { first_point_in_coset += &term; } - first_point_in_coset *= &FpVar::Constant(self.offset); + first_point_in_coset *= &self.offset; coset.push(first_point_in_coset); for i in 1..(1 << (coset_dim as usize)) { diff --git a/src/poly/evaluations/univariate/lagrange_interpolator.rs b/src/poly/evaluations/univariate/lagrange_interpolator.rs index 9a80274..8acd68c 100644 --- a/src/poly/evaluations/univariate/lagrange_interpolator.rs +++ b/src/poly/evaluations/univariate/lagrange_interpolator.rs @@ -98,8 +98,11 @@ impl LagrangeInterpolator { #[cfg(test)] mod tests { - use crate::poly::domain::Radix2Domain; + use crate::fields::fp::FpVar; + use crate::fields::FieldVar; + use crate::poly::domain::Radix2DomainVar; use crate::poly::evaluations::univariate::lagrange_interpolator::LagrangeInterpolator; + use crate::R1CSVar; use ark_ff::{FftField, Field, One}; use ark_poly::univariate::DensePolynomial; use ark_poly::{Polynomial, UVPolynomial}; @@ -112,21 +115,25 @@ mod tests { let poly = DensePolynomial::rand(15, &mut rng); let gen = Fr::get_root_of_unity(1 << 4).unwrap(); assert_eq!(gen.pow(&[1 << 4]), Fr::one()); - let domain = Radix2Domain { + let domain = Radix2DomainVar { gen, - offset: Fr::multiplicative_generator(), + offset: FpVar::constant(Fr::multiplicative_generator()), dim: 4, // 2^4 = 16 }; // generate evaluations of `poly` on this domain - let mut coset_point = domain.offset; + let mut coset_point = domain.offset.value().unwrap(); let mut oracle_evals = Vec::new(); for _ in 0..(1 << 4) { oracle_evals.push(poly.evaluate(&coset_point)); coset_point *= gen; } - let interpolator = - LagrangeInterpolator::new(domain.offset, domain.gen, domain.dim, oracle_evals); + let interpolator = LagrangeInterpolator::new( + domain.offset.value().unwrap(), + domain.gen, + domain.dim, + oracle_evals, + ); // the point to evaluate at let interpolate_point = Fr::rand(&mut rng); diff --git a/src/poly/evaluations/univariate/mod.rs b/src/poly/evaluations/univariate/mod.rs index 32f67bc..295adac 100644 --- a/src/poly/evaluations/univariate/mod.rs +++ b/src/poly/evaluations/univariate/mod.rs @@ -3,7 +3,7 @@ pub mod lagrange_interpolator; use crate::alloc::AllocVar; use crate::fields::fp::FpVar; use crate::fields::FieldVar; -use crate::poly::domain::Radix2Domain; +use crate::poly::domain::Radix2DomainVar; use crate::poly::evaluations::univariate::lagrange_interpolator::LagrangeInterpolator; use crate::R1CSVar; use ark_ff::{batch_inversion, PrimeField}; @@ -18,7 +18,12 @@ pub struct EvaluationsVar { pub evals: Vec>, /// Optional Lagrange Interpolator. Useful for lagrange interpolation. pub lagrange_interpolator: Option>, - domain: Radix2Domain, + domain: Radix2DomainVar, + /// Contains all domain elements of `domain.base_domain`. + /// + /// This is a cache for lagrange interpolation when offset is non-constant. Will be `None` if offset is constant + /// or `interpolate` is set to `false`. + subgroup_points: Option>, } impl EvaluationsVar { @@ -27,7 +32,7 @@ impl EvaluationsVar { /// using lagrange interpolation. pub fn from_vec_and_domain( evaluations: Vec>, - domain: Radix2Domain, + domain: Radix2DomainVar, interpolate: bool, ) -> Self { assert_eq!( @@ -40,22 +45,39 @@ impl EvaluationsVar { evals: evaluations, lagrange_interpolator: None, domain, + subgroup_points: None, }; if interpolate { - ev.generate_lagrange_interpolator(); + ev.generate_interpolation_cache(); } ev } - /// Generate lagrange interpolator and mark it ready to interpolate - pub fn generate_lagrange_interpolator(&mut self) { - let poly_evaluations_val: Vec<_> = self.evals.iter().map(|v| v.value().unwrap()).collect(); - let domain = &self.domain; - let lagrange_interpolator = - LagrangeInterpolator::new(domain.offset, domain.gen, domain.dim, poly_evaluations_val); - self.lagrange_interpolator = Some(lagrange_interpolator) + /// Precompute necessary calculation for lagrange interpolation and mark it ready to interpolate + pub fn generate_interpolation_cache(&mut self) { + if self.domain.offset.is_constant() { + let poly_evaluations_val: Vec<_> = + self.evals.iter().map(|v| v.value().unwrap()).collect(); + let domain = &self.domain; + let lagrange_interpolator = if let FpVar::Constant(x) = domain.offset { + LagrangeInterpolator::new(x, domain.gen, domain.dim, poly_evaluations_val) + } else { + panic!("Domain offset needs to be constant.") + }; + self.lagrange_interpolator = Some(lagrange_interpolator) + } else { + // calculate all elements of base subgroup so that in later part we don't need to calculate the exponents again + let mut subgroup_points = Vec::with_capacity(self.domain.size() as usize); + subgroup_points.push(F::one()); + for i in 1..self.domain.size() as usize { + subgroup_points.push(subgroup_points[i - 1] * self.domain.gen) + } + self.subgroup_points = Some(subgroup_points) + } } + /// Compute lagrange coefficients for each evaluation, given `interpolation_point`. + /// Only valid if the domain offset is constant. fn compute_lagrange_coefficients( &self, interpolation_point: &FpVar, @@ -67,7 +89,7 @@ impl EvaluationsVar { .lagrange_interpolator .as_ref() .expect("lagrange interpolator has not been initialized. \ - Call `self.generate_lagrange_interpolator` first or set `interpolate` to true in constructor. "); + Call `self.generate_interpolation_cache` first or set `interpolate` to true in constructor. "); let lagrange_coeffs = lagrange_interpolator.compute_lagrange_coefficients(t.value().unwrap()); let mut lagrange_coeffs_fg = Vec::new(); @@ -105,6 +127,19 @@ impl EvaluationsVar { pub fn interpolate_and_evaluate( &self, interpolation_point: &FpVar, + ) -> Result, SynthesisError> { + // specialize: if domain offset is constant, we can optimize to have fewer constraints + if self.domain.offset.is_constant() { + self.lagrange_interpolate_with_constant_offset(interpolation_point) + } else { + // if domain offset is not constant, then we use standard lagrange interpolation code + self.lagrange_interpolate_with_non_constant_offset(interpolation_point) + } + } + + fn lagrange_interpolate_with_constant_offset( + &self, + interpolation_point: &FpVar, ) -> Result, SynthesisError> { let lagrange_interpolator = self .lagrange_interpolator @@ -119,6 +154,50 @@ impl EvaluationsVar { Ok(interpolation) } + + /// Generate interpolation constraints. We assume at compile time we know the base coset (i.e. `gen`) but not know `offset`. + fn lagrange_interpolate_with_non_constant_offset( + &self, + interpolation_point: &FpVar, + ) -> Result, SynthesisError> { + // first, make sure `subgroup_points` is made + let subgroup_points = self.subgroup_points.as_ref() + .expect("lagrange interpolator has not been initialized. \ + Call `self.generate_interpolation_cache` first or set `interpolate` to true in constructor. "); + // Let denote interpolation_point as alpha. + // Lagrange polynomial for coset element `a` is + // \frac{1}{size * offset ^ size} * \frac{alpha^size - offset^size}{alpha * a^{-1} - 1} + // Notice that a = (offset * a') where a' is the corresponding element of base coset + + // let `lhs` become \frac{alpha^size - offset^size}{size * offset ^ size}. This part is shared by all lagrange polynomials + let coset_offset_to_size = self.domain.offset.pow_by_constant(&[self.domain.size()])?; // offset^size + let alpha_to_s = interpolation_point.pow_by_constant(&[self.domain.size()])?; + let lhs_numerator = &alpha_to_s - &coset_offset_to_size; + let lhs_denominator = &coset_offset_to_size * FpVar::constant(F::from(self.domain.size())); + + let lhs = lhs_numerator.mul_by_inverse(&lhs_denominator)?; + + // `rhs` for coset element `a` is \frac{1}{alpha * a^{-1} - 1} = \frac{1}{alpha * offset^{-1} * a'^{-1} - 1} + let alpha_coset_offset_inv = interpolation_point.mul_by_inverse(&self.domain.offset)?; + + // `res` stores the sum of all lagrange polynomials evaluated at alpha + let mut res = FpVar::::zero(); + + let domain_size = self.domain.size() as usize; + for i in 0..domain_size { + // a'^{-1} where a is the base coset element + let subgroup_point_inv = subgroup_points[(domain_size - i) % domain_size]; + debug_assert_eq!(subgroup_points[i] * subgroup_point_inv, F::one()); + // alpha * offset^{-1} * a'^{-1} - 1 + let lag_donom = &alpha_coset_offset_inv * subgroup_point_inv - F::one(); + let lag_coeff = lhs.mul_by_inverse(&lag_donom)?; + + let lag_interpoland = &self.evals[i] * lag_coeff; + res += lag_interpoland + } + + Ok(res) + } } impl<'a, 'b, F: PrimeField> Add<&'a EvaluationsVar> for &'b EvaluationsVar { @@ -132,8 +211,13 @@ impl<'a, 'b, F: PrimeField> Add<&'a EvaluationsVar> for &'b EvaluationsVar } impl<'a, F: PrimeField> AddAssign<&'a EvaluationsVar> for EvaluationsVar { + /// Performs the `+=` operations, assuming `domain.offset` is equal. fn add_assign(&mut self, other: &'a EvaluationsVar) { - assert_eq!(self.domain, other.domain, "domains are unequal"); + // offset might be unknown at compile time, so we assume offset is equal + assert!( + self.domain.gen == other.domain.gen && self.domain.dim == other.domain.dim, + "domains are unequal" + ); self.lagrange_interpolator = None; self.evals @@ -154,8 +238,13 @@ impl<'a, 'b, F: PrimeField> Sub<&'a EvaluationsVar> for &'b EvaluationsVar } impl<'a, F: PrimeField> SubAssign<&'a EvaluationsVar> for EvaluationsVar { + /// Performs the `-=` operations, assuming `domain.offset` is equal. fn sub_assign(&mut self, other: &'a EvaluationsVar) { - assert_eq!(self.domain, other.domain, "domains are unequal"); + // offset might be unknown at compile time, so we assume offset is equal + assert!( + self.domain.gen == other.domain.gen && self.domain.dim == other.domain.dim, + "domains are unequal" + ); self.lagrange_interpolator = None; self.evals @@ -168,6 +257,7 @@ impl<'a, F: PrimeField> SubAssign<&'a EvaluationsVar> for EvaluationsVar { impl<'a, 'b, F: PrimeField> Mul<&'a EvaluationsVar> for &'b EvaluationsVar { type Output = EvaluationsVar; + /// Performs the `*` operations, assuming `domain.offset` is equal. fn mul(self, rhs: &'a EvaluationsVar) -> Self::Output { let mut result = self.clone(); result *= rhs; @@ -176,8 +266,13 @@ impl<'a, 'b, F: PrimeField> Mul<&'a EvaluationsVar> for &'b EvaluationsVar } impl<'a, F: PrimeField> MulAssign<&'a EvaluationsVar> for EvaluationsVar { + /// Performs the `*=` operations, assuming `domain.offset` is equal. fn mul_assign(&mut self, other: &'a EvaluationsVar) { - assert_eq!(self.domain, other.domain, "domains are unequal"); + // offset might be unknown at compile time, so we assume offset is equal + assert!( + self.domain.gen == other.domain.gen && self.domain.dim == other.domain.dim, + "domains are unequal" + ); self.lagrange_interpolator = None; self.evals @@ -198,8 +293,13 @@ impl<'a, 'b, F: PrimeField> Div<&'a EvaluationsVar> for &'b EvaluationsVar } impl<'a, F: PrimeField> DivAssign<&'a EvaluationsVar> for EvaluationsVar { + /// Performs the `/=` operations, assuming `domain.offset` is equal. fn div_assign(&mut self, other: &'a EvaluationsVar) { - assert_eq!(self.domain, other.domain, "domains are unequal"); + // offset might be unknown at compile time, so we assume offset is equal + assert!( + self.domain.gen == other.domain.gen && self.domain.dim == other.domain.dim, + "domains are unequal" + ); let cs = self.evals[0].cs(); // the prover can generate result = (1 / other) * self offline let mut result_val: Vec<_> = other.evals.iter().map(|x| x.value().unwrap()).collect(); @@ -228,7 +328,8 @@ impl<'a, F: PrimeField> DivAssign<&'a EvaluationsVar> for EvaluationsVar { mod tests { use crate::alloc::AllocVar; use crate::fields::fp::FpVar; - use crate::poly::domain::Radix2Domain; + use crate::fields::FieldVar; + use crate::poly::domain::Radix2DomainVar; use crate::poly::evaluations::univariate::EvaluationsVar; use crate::R1CSVar; use ark_ff::{FftField, Field, One, UniformRand}; @@ -239,17 +340,17 @@ mod tests { use ark_test_curves::bls12_381::Fr; #[test] - fn test_interpolate() { + fn test_interpolate_constant_offset() { let mut rng = test_rng(); let poly = DensePolynomial::rand(15, &mut rng); let gen = Fr::get_root_of_unity(1 << 4).unwrap(); assert_eq!(gen.pow(&[1 << 4]), Fr::one()); - let domain = Radix2Domain { + let domain = Radix2DomainVar { gen, - offset: Fr::multiplicative_generator(), + offset: FpVar::constant(Fr::rand(&mut rng)), dim: 4, // 2^4 = 16 }; - let mut coset_point = domain.offset; + let mut coset_point = domain.offset.value().unwrap(); let mut oracle_evals = Vec::new(); for _ in 0..(1 << 4) { oracle_evals.push(poly.evaluate(&coset_point)); @@ -276,6 +377,49 @@ mod tests { assert_eq!(actual, expected); assert!(cs.is_satisfied().unwrap()); + println!("number of constraints: {}", cs.num_constraints()) + } + + #[test] + fn test_interpolate_non_constant_offset() { + let mut rng = test_rng(); + let poly = DensePolynomial::rand(15, &mut rng); + let gen = Fr::get_root_of_unity(1 << 4).unwrap(); + assert_eq!(gen.pow(&[1 << 4]), Fr::one()); + let cs = ConstraintSystem::new_ref(); + let domain = Radix2DomainVar { + gen, + offset: FpVar::new_witness(ns!(cs, "offset"), || Ok(Fr::rand(&mut rng))).unwrap(), + dim: 4, // 2^4 = 16 + }; + let mut coset_point = domain.offset.value().unwrap(); + let mut oracle_evals = Vec::new(); + for _ in 0..(1 << 4) { + oracle_evals.push(poly.evaluate(&coset_point)); + coset_point *= gen; + } + + let evaluations_fp: Vec<_> = oracle_evals + .iter() + .map(|x| FpVar::new_input(ns!(cs, "evaluations"), || Ok(x)).unwrap()) + .collect(); + let evaluations_var = EvaluationsVar::from_vec_and_domain(evaluations_fp, domain, true); + + let interpolate_point = Fr::rand(&mut rng); + let interpolate_point_fp = + FpVar::new_input(ns!(cs, "interpolate point"), || Ok(interpolate_point)).unwrap(); + + let expected = poly.evaluate(&interpolate_point); + + let actual = evaluations_var + .interpolate_and_evaluate(&interpolate_point_fp) + .unwrap() + .value() + .unwrap(); + + assert_eq!(actual, expected); + assert!(cs.is_satisfied().unwrap()); + println!("number of constraints: {}", cs.num_constraints()) } #[test] @@ -283,9 +427,9 @@ mod tests { let mut rng = test_rng(); let gen = Fr::get_root_of_unity(1 << 4).unwrap(); assert_eq!(gen.pow(&[1 << 4]), Fr::one()); - let domain = Radix2Domain { + let domain = Radix2DomainVar { gen, - offset: Fr::multiplicative_generator(), + offset: FpVar::constant(Fr::multiplicative_generator()), dim: 4, // 2^4 = 16 };