Skip to content

Commit 1b11dd2

Browse files
srinathsettyhuitseeker
authored andcommitted
Improve sparse polynomial evaluation algorithm (#317)
* time-optimal algorithm for sparse polynomial evaluation * update version
1 parent a7cbbda commit 1b11dd2

File tree

6 files changed

+52
-87
lines changed

6 files changed

+52
-87
lines changed

src/spartan/batched.rs

+1-11
Original file line numberDiff line numberDiff line change
@@ -490,17 +490,7 @@ impl<E: Engine, EE: EvaluationEngineTrait<E>> BatchedRelaxedR1CSSNARKTrait<E>
490490
let evals_Z = zip_with!(iter, (self.evals_W, U, r_y), |eval_W, U, r_y| {
491491
let eval_X = {
492492
// constant term
493-
let poly_X = iter::once((0, U.u))
494-
.chain(
495-
//remaining inputs
496-
U.X
497-
.iter()
498-
.enumerate()
499-
// filter_map uses the sparsity of the polynomial, if irrelevant
500-
// we should replace by UniPoly
501-
.filter_map(|(i, x_i)| (!x_i.is_zero_vartime()).then_some((i + 1, *x_i))),
502-
)
503-
.collect();
493+
let poly_X = iter::once(U.u).chain(U.X.iter().cloned()).collect();
504494
SparsePolynomial::new(r_y.len() - 1, poly_X).evaluate(&r_y[1..])
505495
};
506496
(E::Scalar::ONE - r_y[0]) * eval_W + r_y[0] * eval_X

src/spartan/batched_ppsnark.rs

+1-9
Original file line numberDiff line numberDiff line change
@@ -927,15 +927,7 @@ impl<E: Engine, EE: EvaluationEngineTrait<E>> BatchedRelaxedR1CSSNARKTrait<E>
927927

928928
let X = {
929929
// constant term
930-
let poly_X = std::iter::once((0, U.u))
931-
.chain(
932-
//remaining inputs
933-
(0..U.X.len())
934-
// filter_map uses the sparsity of the polynomial, if irrelevant
935-
// we should replace by UniPoly
936-
.filter_map(|i| (!U.X[i].is_zero_vartime()).then_some((i + 1, U.X[i]))),
937-
)
938-
.collect();
930+
let poly_X = std::iter::once(U.u).chain(U.X.iter().cloned()).collect();
939931
SparsePolynomial::new(num_vars_log, poly_X).evaluate(&rand_sc_unpad[1..])
940932
};
941933

src/spartan/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ use crate::{
2323
};
2424
use ff::Field;
2525
use itertools::Itertools as _;
26-
use polys::multilinear::SparsePolynomial;
2726
use rayon::{iter::IntoParallelRefIterator, prelude::*};
2827
use rayon_scan::ScanParallelIterator as _;
2928
use ref_cast::RefCast;

src/spartan/polys/multilinear.rs

+32-41
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ use ff::PrimeField;
88
use itertools::Itertools as _;
99
use rand_core::{CryptoRng, RngCore};
1010
use rayon::prelude::{
11-
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator,
12-
IntoParallelRefMutIterator, ParallelIterator,
11+
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator,
1312
};
1413
use serde::{Deserialize, Serialize};
1514

@@ -130,47 +129,36 @@ impl<Scalar: PrimeField> Index<usize> for MultilinearPolynomial<Scalar> {
130129
}
131130

132131
/// Sparse multilinear polynomial, which means the $Z(\cdot)$ is zero at most points.
133-
/// So we do not have to store every evaluations of $Z(\cdot)$, only store the non-zero points.
134-
///
135-
/// For example, the evaluations are [0, 0, 0, 1, 0, 1, 0, 2].
136-
/// The sparse polynomial only store the non-zero values, [(3, 1), (5, 1), (7, 2)].
137-
/// In the tuple, the first is index, the second is value.
132+
/// In our context, sparse polynomials are non-zeros over the hypercube at locations that map to "small" integers
133+
/// We exploit this property to implement a time-optimal algorithm
138134
pub(crate) struct SparsePolynomial<Scalar> {
139135
num_vars: usize,
140-
Z: Vec<(usize, Scalar)>,
136+
Z: Vec<Scalar>,
141137
}
142138

143139
impl<Scalar: PrimeField> SparsePolynomial<Scalar> {
144-
pub fn new(num_vars: usize, Z: Vec<(usize, Scalar)>) -> Self {
140+
pub fn new(num_vars: usize, Z: Vec<Scalar>) -> Self {
145141
Self { num_vars, Z }
146142
}
147143

148-
/// Computes the $\tilde{eq}$ extension polynomial.
149-
/// return 1 when a == r, otherwise return 0.
150-
fn compute_chi(a: &[bool], r: &[Scalar]) -> Scalar {
151-
assert_eq!(a.len(), r.len());
152-
let mut chi_i = Scalar::ONE;
153-
for j in 0..r.len() {
154-
if a[j] {
155-
chi_i *= r[j];
156-
} else {
157-
chi_i *= Scalar::ONE - r[j];
158-
}
159-
}
160-
chi_i
161-
}
162-
163-
// Takes O(m log n) where m is the number of non-zero evaluations and n is the number of variables.
144+
// a time-optimal algorithm to evaluate sparse polynomials
164145
pub fn evaluate(&self, r: &[Scalar]) -> Scalar {
165146
assert_eq!(self.num_vars, r.len());
166147

167-
(0..self.Z.len())
168-
.into_par_iter()
169-
.map(|i| {
170-
let bits = (self.Z[i].0).get_bits(r.len());
171-
Self::compute_chi(&bits, r) * self.Z[i].1
172-
})
173-
.sum()
148+
let num_vars_z = self.Z.len().next_power_of_two().log_2();
149+
let chis = EqPolynomial::evals_from_points(&r[self.num_vars - 1 - num_vars_z..]);
150+
let eval_partial: Scalar = self
151+
.Z
152+
.iter()
153+
.zip_eq(chis.iter())
154+
.map(|(z, chi)| *z * *chi)
155+
.sum();
156+
157+
let common = (0..self.num_vars - 1 - num_vars_z)
158+
.map(|i| (Scalar::ONE - r[i]))
159+
.product::<Scalar>();
160+
161+
common * eval_partial
174162
}
175163
}
176164

@@ -232,18 +220,21 @@ mod tests {
232220
}
233221

234222
fn test_sparse_polynomial_with<F: PrimeField>() {
235-
// Let the polynomial have 3 variables, p(x_1, x_2, x_3) = (x_1 + x_2) * x_3
236-
// Evaluations of the polynomial at boolean cube are [0, 0, 0, 1, 0, 1, 0, 2].
223+
// Let the polynomial have 4 variables, but is non-zero at only 3 locations (out of 2^4 = 16) over the hypercube
224+
let mut Z = vec![F::ONE, F::ONE, F::from(2)];
225+
let m_poly = SparsePolynomial::<F>::new(4, Z.clone());
237226

238-
let TWO = F::from(2);
239-
let Z = vec![(3, F::ONE), (5, F::ONE), (7, TWO)];
240-
let m_poly = SparsePolynomial::<F>::new(3, Z);
227+
Z.resize(16, F::ZERO); // append with zeros to make it a dense polynomial
228+
let m_poly_dense = MultilinearPolynomial::new(Z);
241229

242-
let x = vec![F::ONE, F::ONE, F::ONE];
243-
assert_eq!(m_poly.evaluate(x.as_slice()), TWO);
230+
// evaluation point
231+
let x = vec![F::from(5), F::from(8), F::from(5), F::from(3)];
244232

245-
let x = vec![F::ONE, F::ZERO, F::ONE];
246-
assert_eq!(m_poly.evaluate(x.as_slice()), F::ONE);
233+
// check evaluations
234+
assert_eq!(
235+
m_poly.evaluate(x.as_slice()),
236+
m_poly_dense.evaluate(x.as_slice())
237+
);
247238
}
248239

249240
#[test]

src/spartan/ppsnark.rs

+11-13
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::{
2424
},
2525
SumcheckProof,
2626
},
27-
PolyEvalInstance, PolyEvalWitness, SparsePolynomial,
27+
PolyEvalInstance, PolyEvalWitness,
2828
},
2929
traits::{
3030
commitment::{CommitmentEngineTrait, CommitmentTrait, Len},
@@ -42,7 +42,7 @@ use rayon::prelude::*;
4242
use serde::{Deserialize, Serialize};
4343
use std::sync::Arc;
4444

45-
use super::polys::masked_eq::MaskedEqPolynomial;
45+
use super::polys::{masked_eq::MaskedEqPolynomial, multilinear::SparsePolynomial};
4646

4747
fn padded<E: Engine>(v: &[E::Scalar], n: usize, e: &E::Scalar) -> Vec<E::Scalar> {
4848
let mut v_padded = vec![*e; n];
@@ -930,17 +930,15 @@ impl<E: Engine, EE: EvaluationEngineTrait<E>> RelaxedR1CSSNARKTrait<E> for Relax
930930
};
931931

932932
let eval_X = {
933-
// constant term
934-
let poly_X = std::iter::once((0, U.u))
935-
.chain(
936-
//remaining inputs
937-
(0..U.X.len())
938-
// filter_map uses the sparsity of the polynomial, if irrelevant
939-
// we should replace by UniPoly
940-
.filter_map(|i| (!U.X[i].is_zero_vartime()).then_some((i + 1, U.X[i]))),
941-
)
942-
.collect();
943-
SparsePolynomial::new(vk.num_vars.log_2(), poly_X).evaluate(&rand_sc_unpad[1..])
933+
// public IO is (u, X)
934+
let X = vec![U.u]
935+
.into_iter()
936+
.chain(U.X.iter().cloned())
937+
.collect::<Vec<E::Scalar>>();
938+
939+
// evaluate the sparse polynomial at rand_sc_unpad[1..]
940+
let poly_X = SparsePolynomial::new(rand_sc_unpad.len() - 1, X);
941+
poly_X.evaluate(&rand_sc_unpad[1..])
944942
};
945943

946944
self.eval_W + factor * rand_sc_unpad[0] * eval_X

src/spartan/snark.rs

+7-12
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use itertools::Itertools as _;
3232
use once_cell::sync::OnceCell;
3333
use rayon::prelude::*;
3434
use serde::{Deserialize, Serialize};
35-
use std::{iter, sync::Arc};
35+
use std::sync::Arc;
3636

3737
/// A type that represents the prover's key
3838
#[derive(Debug, Clone)]
@@ -328,17 +328,12 @@ impl<E: Engine, EE: EvaluationEngineTrait<E>> RelaxedR1CSSNARKTrait<E> for Relax
328328
// verify claim_inner_final
329329
let eval_Z = {
330330
let eval_X = {
331-
// constant term
332-
let poly_X = iter::once((0, U.u))
333-
.chain(
334-
//remaining inputs
335-
(0..U.X.len())
336-
// filter_map uses the sparsity of the polynomial, if irrelevant
337-
// we should replace by UniPoly
338-
.filter_map(|i| (!U.X[i].is_zero_vartime()).then_some((i + 1, U.X[i]))),
339-
)
340-
.collect();
341-
SparsePolynomial::new(usize::try_from(vk.S.num_vars.ilog2()).unwrap(), poly_X)
331+
// public IO is (u, X)
332+
let X = vec![U.u]
333+
.into_iter()
334+
.chain(U.X.iter().cloned())
335+
.collect::<Vec<E::Scalar>>();
336+
SparsePolynomial::new(usize::try_from(vk.S.num_vars.ilog2()).unwrap(), X)
342337
.evaluate(&r_y[1..])
343338
};
344339
(E::Scalar::ONE - r_y[0]) * self.eval_W + r_y[0] * eval_X

0 commit comments

Comments
 (0)