Skip to content

Commit 989f579

Browse files
Add Univariate Domain, Vanishing Polynomial, Lagrange Interpolation (#53)
* add domain and vp * add lagrange interpolator * add query position to coset * nostd * add test assertion * fmt * fix test * add Add and Sub arithmetic * add Add and Sub arithmetic * add unit test for mul/div arithmetic * add more doc for clarification * add test for native interpolate * add test for vp constraints * fix lagrange interpolate bug * comment cleanup + fmt * add CHANGELOG * fix a compile error * Update CHANGELOG.md * Update CHANGELOG.md * fix comment * doc fix * doc update 2 * doc update 3 * pub lagrange_interpolator * doc fix * rename `EvaluationDomain` to `Radix2Domain` * tweak * tweak Co-authored-by: weikeng <[email protected]>
1 parent d1be6d1 commit 989f579

File tree

8 files changed

+624
-1
lines changed

8 files changed

+624
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ You can update downstream usage with `grep -rl 'AllocatedBit' . | xargs env LANG
77

88
### Features
99

10+
- [\#53](https://github.com/arkworks-rs/r1cs-std/pull/53) Add univariate evaluation domain and lagrange interpolation.
11+
1012
### Improvements
1113

1214
### Bug Fixes

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ ark-poly = { version = "^0.2.0", default-features = false }
3636
[features]
3737
default = ["std"]
3838
std = [ "ark-ff/std", "ark-relations/std", "ark-std/std", "num-bigint/std" ]
39-
parallel = [ "std", "ark-ff/parallel" ]
39+
parallel = [ "std", "ark-ff/parallel", "ark-std/parallel"]

src/poly/domain/mod.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::boolean::Boolean;
2+
use crate::fields::fp::FpVar;
3+
use crate::fields::FieldVar;
4+
use ark_ff::PrimeField;
5+
use ark_relations::r1cs::SynthesisError;
6+
use ark_std::vec::Vec;
7+
8+
pub mod vanishing_poly;
9+
10+
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
11+
/// Defines an evaluation domain over a prime field. The domain is a coset of size `1<<dim`.
12+
///
13+
/// Native code corresponds to `ark-poly::univariate::domain::radix2`, but `ark-poly` only supports
14+
/// subgroup for now.
15+
///
16+
/// TODO: support cosets in `ark-poly`.
17+
pub struct Radix2Domain<F: PrimeField> {
18+
/// generator of subgroup g
19+
pub gen: F,
20+
/// index of the quotient group (i.e. the `offset`)
21+
pub offset: F,
22+
/// dimension of evaluation domain
23+
pub dim: u64,
24+
}
25+
26+
impl<F: PrimeField> Radix2Domain<F> {
27+
/// order of the domain
28+
pub fn order(&self) -> usize {
29+
1 << self.dim
30+
}
31+
32+
/// Returns g, g^2, ..., g^{dim}
33+
fn powers_of_gen(&self, dim: usize) -> Vec<F> {
34+
let mut result = Vec::new();
35+
let mut cur = self.gen;
36+
for _ in 0..dim {
37+
result.push(cur);
38+
cur = cur * cur;
39+
}
40+
result
41+
}
42+
43+
/// For domain `h<g>` with dimension `n`, `position` represented by `query_pos` in big endian form,
44+
/// returns `h*g^{position}<g^{n-query_pos.len()}>`
45+
pub fn query_position_to_coset(
46+
&self,
47+
query_pos: &[Boolean<F>],
48+
coset_dim: u64,
49+
) -> Result<Vec<FpVar<F>>, SynthesisError> {
50+
let mut coset_index = query_pos;
51+
assert!(
52+
query_pos.len() == self.dim as usize
53+
|| query_pos.len() == (self.dim - coset_dim) as usize
54+
);
55+
if query_pos.len() == self.dim as usize {
56+
coset_index = &coset_index[0..(coset_index.len() - coset_dim as usize)];
57+
}
58+
let mut coset = Vec::new();
59+
let powers_of_g = &self.powers_of_gen(self.dim as usize)[(coset_dim as usize)..];
60+
61+
let mut first_point_in_coset: FpVar<F> = FpVar::zero();
62+
for i in 0..coset_index.len() {
63+
let term = coset_index[i].select(&FpVar::constant(powers_of_g[i]), &FpVar::zero())?;
64+
first_point_in_coset += &term;
65+
}
66+
67+
first_point_in_coset *= &FpVar::Constant(self.offset);
68+
69+
coset.push(first_point_in_coset);
70+
for i in 1..(1 << (coset_dim as usize)) {
71+
let new_elem = &coset[i - 1] * &FpVar::Constant(self.gen);
72+
coset.push(new_elem);
73+
}
74+
75+
Ok(coset)
76+
}
77+
}

src/poly/domain/vanishing_poly.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use crate::fields::fp::FpVar;
2+
use crate::fields::FieldVar;
3+
use ark_ff::{Field, PrimeField};
4+
use ark_relations::r1cs::SynthesisError;
5+
use ark_std::ops::Sub;
6+
7+
/// Struct describing vanishing polynomial for a multiplicative coset H where |H| is a power of 2.
8+
/// As H is a coset, every element can be described as h*g^i and therefore
9+
/// has vanishing polynomial Z_H(x) = x^|H| - h^|H|
10+
#[derive(Clone)]
11+
pub struct VanishingPolynomial<F: Field> {
12+
/// h^|H|
13+
pub constant_term: F,
14+
/// log_2(|H|)
15+
pub dim_h: u64,
16+
/// |H|
17+
pub order_h: u64,
18+
}
19+
20+
impl<F: PrimeField> VanishingPolynomial<F> {
21+
/// returns a VanishingPolynomial of coset `H = h<g>`.
22+
pub fn new(offset: F, dim_h: u64) -> Self {
23+
let order_h = 1 << dim_h;
24+
let vp = VanishingPolynomial {
25+
constant_term: offset.pow([order_h]),
26+
dim_h,
27+
order_h,
28+
};
29+
vp
30+
}
31+
32+
/// Evaluates the vanishing polynomial without generating the constraints.
33+
pub fn evaluate(&self, x: &F) -> F {
34+
let mut result = x.pow([self.order_h]);
35+
result -= &self.constant_term;
36+
result
37+
}
38+
39+
/// Evaluates the constraints and just gives you the gadget for the result.
40+
/// Caution for use in holographic lincheck: The output has 2 entries in one matrix
41+
pub fn evaluate_constraints(&self, x: &FpVar<F>) -> Result<FpVar<F>, SynthesisError> {
42+
if self.dim_h == 1 {
43+
let result = x.sub(x);
44+
return Ok(result);
45+
}
46+
47+
let mut cur = x.square()?;
48+
for _ in 1..self.dim_h {
49+
cur.square_in_place()?;
50+
}
51+
cur -= &FpVar::Constant(self.constant_term);
52+
Ok(cur)
53+
}
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use crate::alloc::AllocVar;
59+
use crate::fields::fp::FpVar;
60+
use crate::poly::domain::vanishing_poly::VanishingPolynomial;
61+
use crate::R1CSVar;
62+
use ark_relations::r1cs::ConstraintSystem;
63+
use ark_std::{test_rng, UniformRand};
64+
use ark_test_curves::bls12_381::Fr;
65+
66+
#[test]
67+
fn constraints_test() {
68+
let mut rng = test_rng();
69+
let offset = Fr::rand(&mut rng);
70+
let cs = ConstraintSystem::new_ref();
71+
let x = Fr::rand(&mut rng);
72+
let x_var = FpVar::new_witness(ns!(cs, "x_var"), || Ok(x)).unwrap();
73+
let vp = VanishingPolynomial::new(offset, 12);
74+
let native = vp.evaluate(&x);
75+
let result_var = vp.evaluate_constraints(&x_var).unwrap();
76+
assert!(cs.is_satisfied().unwrap());
77+
assert_eq!(result_var.value().unwrap(), native);
78+
}
79+
}

src/poly/evaluations/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod univariate;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use crate::poly::domain::vanishing_poly::VanishingPolynomial;
2+
use ark_ff::{batch_inversion, PrimeField};
3+
use ark_std::vec::Vec;
4+
/// Struct describing Lagrange interpolation for a multiplicative coset I,
5+
/// with |I| a power of 2.
6+
/// TODO: Pull in lagrange poly explanation from libiop
7+
#[derive(Clone)]
8+
pub struct LagrangeInterpolator<F: PrimeField> {
9+
pub(crate) domain_order: usize,
10+
pub(crate) all_domain_elems: Vec<F>,
11+
pub(crate) v_inv_elems: Vec<F>,
12+
pub(crate) domain_vp: VanishingPolynomial<F>,
13+
poly_evaluations: Vec<F>,
14+
}
15+
16+
impl<F: PrimeField> LagrangeInterpolator<F> {
17+
/// Returns a lagrange interpolator, given the domain specification.
18+
pub fn new(
19+
domain_offset: F,
20+
domain_generator: F,
21+
domain_dim: u64,
22+
poly_evaluations: Vec<F>,
23+
) -> Self {
24+
let domain_order = 1 << domain_dim;
25+
assert_eq!(poly_evaluations.len(), domain_order);
26+
let mut cur_elem = domain_offset;
27+
let mut all_domain_elems = vec![domain_offset];
28+
let mut v_inv_elems: Vec<F> = Vec::new();
29+
// Cache all elements in the domain
30+
for _ in 1..domain_order {
31+
cur_elem *= domain_generator;
32+
all_domain_elems.push(cur_elem);
33+
}
34+
/*
35+
By computing the following elements as constants,
36+
we can further reduce the interpolation costs.
37+
38+
m = order of the interpolation domain
39+
v_inv[i] = prod_{j != i} h(g^i - g^j)
40+
We use the following facts to compute this:
41+
v_inv[0] = m*h^{m-1}
42+
v_inv[i] = g^{-1} * v_inv[i-1]
43+
*/
44+
// TODO: Include proof of the above two points
45+
let g_inv = domain_generator.inverse().unwrap();
46+
let m = F::from((1 << domain_dim) as u128);
47+
let mut v_inv_i = m * domain_offset.pow([(domain_order - 1) as u64]);
48+
for _ in 0..domain_order {
49+
v_inv_elems.push(v_inv_i);
50+
v_inv_i *= g_inv;
51+
}
52+
53+
// TODO: Cache the intermediate terms with Z_H(x) evaluations.
54+
let vp = VanishingPolynomial::new(domain_offset, domain_dim);
55+
56+
let lagrange_interpolation: LagrangeInterpolator<F> = LagrangeInterpolator {
57+
domain_order,
58+
all_domain_elems,
59+
v_inv_elems,
60+
domain_vp: vp,
61+
poly_evaluations,
62+
};
63+
lagrange_interpolation
64+
}
65+
66+
pub(crate) fn compute_lagrange_coefficients(&self, interpolation_point: F) -> Vec<F> {
67+
/*
68+
* Let t be the interpolation point, H be the multiplicative coset, with elements of the form h*g^i.
69+
Compute each L_{i,H}(t) as Z_{H}(t) * v_i / (t- h g^i)
70+
where:
71+
- Z_{H}(t) = \prod_{j} (t-h*g^j) = (t^m-h^m), and
72+
- v_{i} = 1 / \prod_{j \neq i} h(g^i-g^j).
73+
Below we use the fact that v_{0} = 1/(m * h^(m-1)) and v_{i+1} = g * v_{i}.
74+
We compute the inverse of each coefficient, and then batch invert the entire result.
75+
*/
76+
let vp_t_inv = self
77+
.domain_vp
78+
.evaluate(&interpolation_point)
79+
.inverse()
80+
.unwrap();
81+
let mut inverted_lagrange_coeffs: Vec<F> = Vec::with_capacity(self.all_domain_elems.len());
82+
for i in 0..self.domain_order {
83+
let l = vp_t_inv * self.v_inv_elems[i];
84+
let r = self.all_domain_elems[i];
85+
inverted_lagrange_coeffs.push(l * (interpolation_point - r));
86+
}
87+
let lagrange_coeffs = inverted_lagrange_coeffs.as_mut_slice();
88+
batch_inversion::<F>(lagrange_coeffs);
89+
lagrange_coeffs.iter().cloned().collect()
90+
}
91+
92+
pub fn interpolate(&self, interpolation_point: F) -> F {
93+
let lagrange_coeffs = self.compute_lagrange_coefficients(interpolation_point);
94+
let mut interpolation = F::zero();
95+
for i in 0..self.domain_order {
96+
interpolation += lagrange_coeffs[i] * self.poly_evaluations[i];
97+
}
98+
interpolation
99+
}
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use crate::poly::domain::Radix2Domain;
105+
use crate::poly::evaluations::univariate::lagrange_interpolator::LagrangeInterpolator;
106+
use ark_ff::{FftField, Field, One};
107+
use ark_poly::univariate::DensePolynomial;
108+
use ark_poly::{Polynomial, UVPolynomial};
109+
use ark_std::{test_rng, UniformRand};
110+
use ark_test_curves::bls12_381::Fr;
111+
112+
#[test]
113+
pub fn test_native_interpolate() {
114+
let mut rng = test_rng();
115+
let poly = DensePolynomial::rand(15, &mut rng);
116+
let gen = Fr::get_root_of_unity(1 << 4).unwrap();
117+
assert_eq!(gen.pow(&[1 << 4]), Fr::one());
118+
let domain = Radix2Domain {
119+
gen,
120+
offset: Fr::multiplicative_generator(),
121+
dim: 4, // 2^4 = 16
122+
};
123+
// generate evaluations of `poly` on this domain
124+
let mut coset_point = domain.offset;
125+
let mut oracle_evals = Vec::new();
126+
for _ in 0..(1 << 4) {
127+
oracle_evals.push(poly.evaluate(&coset_point));
128+
coset_point *= gen;
129+
}
130+
131+
let interpolator =
132+
LagrangeInterpolator::new(domain.offset, domain.gen, domain.dim, oracle_evals);
133+
134+
// the point to evaluate at
135+
let interpolate_point = Fr::rand(&mut rng);
136+
137+
let expected = poly.evaluate(&interpolate_point);
138+
let actual = interpolator.interpolate(interpolate_point);
139+
140+
assert_eq!(actual, expected)
141+
}
142+
}

0 commit comments

Comments
 (0)