jf_pcs/
poly.rs

1// Copyright (c) 2023 Espresso Systems (espressosys.com)
2// This file is part of the Jellyfish library.
3
4// You should have received a copy of the MIT License
5// along with the Jellyfish library. If not, see <https://mit-license.org/>.
6
7//! Generalized DensePolynomial, allowing coefficients to be group elements, not
8//! just field elements.
9//! Inspired by and natural extension of arkwork's `ark-poly` code.
10
11use ark_ff::{FftField, Field, Zero};
12use ark_poly::{domain::DomainCoeff, EvaluationDomain, Radix2EvaluationDomain};
13use ark_std::{
14    fmt,
15    marker::PhantomData,
16    ops::{Add, Mul, MulAssign},
17    rand::Rng,
18    vec::Vec,
19    UniformRand,
20};
21use itertools::{
22    EitherOrBoth::{Both, Left, Right},
23    Itertools,
24};
25
26use crate::prelude::PCSError;
27
28// TODO: (alex) change to trait alias once stabilized in Rust:
29// `https://doc.rust-lang.org/unstable-book/language-features/trait-alias.html`
30/// A trait bound alias for generalized coefficient type used in
31/// `GeneralDensePolynomial`. Concrete instantiations can be both field or
32/// group elements.
33pub trait GroupCoeff<F: Field>:
34    Clone + fmt::Debug + Copy + Add<Self, Output = Self> + MulAssign<F> + Zero + UniformRand + Sized
35{
36}
37
38impl<T, F: Field> GroupCoeff<F> for T where
39    T: Clone
40        + fmt::Debug
41        + Copy
42        + Add<Self, Output = Self>
43        + MulAssign<F>
44        + Zero
45        + UniformRand
46        + Sized
47{
48}
49
50/// Stores a polynomial in the coefficient form.
51#[derive(Clone, PartialEq, Eq, Default)]
52pub struct GeneralDensePolynomial<T: GroupCoeff<F>, F: Field> {
53    /// The coefficient of `x^i` is stored at location `i` in `self.coeffs`.
54    pub coeffs: Vec<T>,
55    _phantom: PhantomData<F>,
56}
57
58// TODO: (alex) we didn't implement `trait Polynomial<F>` in arkwork for our
59// struct because that trait assume the coeffs are field elements. Therefore, we
60// need to generalize that trait first which is left for future work or even
61// upstream PR.
62impl<T, F> GeneralDensePolynomial<T, F>
63where
64    T: GroupCoeff<F>,
65    F: Field,
66{
67    /// Constructs a new polynomial from a list of coefficients
68    pub fn from_coeff_slice(coeffs: &[T]) -> Self {
69        Self::from_coeff_vec(coeffs.to_vec())
70    }
71
72    /// Constructs a new polynomial from a list of coefficients
73    pub fn from_coeff_vec(coeffs: Vec<T>) -> Self {
74        let mut result = Self {
75            coeffs,
76            _phantom: PhantomData,
77        };
78        result.truncate_leading_zeros();
79        assert!(result.coeffs.last().map_or(true, |coeff| !coeff.is_zero()));
80        result
81    }
82
83    /// Outputs a univariate polynomial of degree `d` where
84    /// each coefficient is sampled uniformly at random.
85    #[allow(dead_code)]
86    pub fn rand<R: Rng>(d: usize, rng: &mut R) -> Self {
87        let mut random_coeffs = Vec::new();
88        for _ in 0..=d {
89            random_coeffs.push(T::rand(rng));
90        }
91        Self::from_coeff_vec(random_coeffs)
92    }
93
94    /// Returns the total degree of the polynomial
95    #[allow(dead_code)]
96    pub fn degree(&self) -> usize {
97        if self.is_zero() {
98            0
99        } else {
100            assert!(self.coeffs.last().map_or(false, |coeff| !coeff.is_zero()));
101            self.coeffs.len() - 1
102        }
103    }
104
105    /// Evaluate `self` at a given `point`
106    pub fn evaluate(&self, point: &F) -> T {
107        if self.is_zero() {
108            return T::zero();
109        } else if point.is_zero() {
110            return self.coeffs[0];
111        }
112        self.horner_evaluate(point)
113    }
114}
115
116impl<T, F> GeneralDensePolynomial<T, F>
117where
118    T: GroupCoeff<F> + DomainCoeff<F> + for<'a> Mul<&'a F, Output = T>,
119    F: FftField,
120{
121    /// Evaluate `self` at a list of arbitrary `points`.
122    pub fn batch_evaluate(&self, points: &[F]) -> Vec<T> {
123        // Horner: n * d
124        // FFT: ~ d*log^2(d)  (independent of n, as long as n<=d)
125        // naive cutoff-point, not taking parallelism into consideration:
126        let cutoff_size = <F as FftField>::TWO_ADICITY.pow(2);
127
128        if points.is_empty() {
129            Vec::new()
130        } else if points.len() < cutoff_size as usize {
131            points.iter().map(|x| self.evaluate(x)).collect()
132        } else {
133            unimplemented!("TODO: (alex) implements Appendix A of FK23");
134        }
135    }
136
137    /// Similar task as [`Self::batch_evaluate()`], except the points are
138    /// [roots of unity](https://en.wikipedia.org/wiki/Root_of_unity) of `domain`.
139    /// By leveraging FFT algorithms, we have a much lower amortized cost.
140    pub fn batch_evaluate_rou(
141        &mut self,
142        domain: &Radix2EvaluationDomain<F>,
143    ) -> Result<Vec<T>, PCSError> {
144        if self.coeffs.len() > domain.size() {
145            Err(PCSError::InvalidParameters(
146                ark_std::format!(
147                    "Polynomial with {} num_of_coeffs can't be evaluated on a smaller domain with size {}",
148                    self.coeffs.len(), domain.size(),
149                )
150            ))
151        } else {
152            self.coeffs.resize_with(domain.size(), Zero::zero);
153            Ok(domain.fft(&self.coeffs))
154        }
155    }
156}
157
158impl<T, F> GeneralDensePolynomial<T, F>
159where
160    T: GroupCoeff<F>,
161    F: Field,
162{
163    fn truncate_leading_zeros(&mut self) {
164        while self.coeffs.last().map_or(false, |c| c.is_zero()) {
165            self.coeffs.pop();
166        }
167    }
168
169    // Horner's method for polynomial evaluation with cost O(n).
170    fn horner_evaluate(&self, point: &F) -> T {
171        self.coeffs
172            .iter()
173            .rfold(T::zero(), move |mut result, coeff| {
174                result *= *point;
175                result + *coeff
176            })
177    }
178}
179
180impl<T, F> Zero for GeneralDensePolynomial<T, F>
181where
182    T: GroupCoeff<F>,
183    F: Field,
184{
185    /// returns the zero polynomial
186    fn zero() -> Self {
187        Self {
188            coeffs: Vec::new(),
189            _phantom: PhantomData,
190        }
191    }
192
193    /// checks if the given polynomial is zero
194    fn is_zero(&self) -> bool {
195        self.coeffs.is_empty() || self.coeffs.iter().all(|coeff| coeff.is_zero())
196    }
197}
198
199impl<T, F> Add<Self> for GeneralDensePolynomial<T, F>
200where
201    T: GroupCoeff<F>,
202    F: Field,
203{
204    type Output = Self;
205
206    // TODO: (alex) add `Add<'a Self, Output=Self>` and internally use that instead.
207    fn add(self, rhs: Self) -> Self::Output {
208        let mut res = if self.is_zero() {
209            rhs
210        } else if rhs.is_zero() {
211            self
212        } else {
213            let coeffs = self
214                .coeffs
215                .into_iter()
216                .zip_longest(rhs.coeffs)
217                .map(|pair| match pair {
218                    Both(x, y) => x + y,
219                    Left(x) | Right(x) => x,
220                })
221                .collect();
222            Self::from_coeff_vec(coeffs)
223        };
224
225        res.truncate_leading_zeros();
226        res
227    }
228}
229
230impl<T, F> fmt::Debug for GeneralDensePolynomial<T, F>
231where
232    T: GroupCoeff<F>,
233    F: Field,
234{
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
236        for (i, coeff) in self.coeffs.iter().enumerate().filter(|(_, c)| !c.is_zero()) {
237            if i == 0 {
238                write!(f, "\n{:?}", coeff)?;
239            } else if i == 1 {
240                write!(f, " + \n{:?} * x", coeff)?;
241            } else {
242                write!(f, " + \n{:?} * x^{}", coeff, i)?;
243            }
244        }
245        Ok(())
246    }
247}
248
249#[cfg(test)]
250pub(crate) mod tests {
251    use super::*;
252    use ark_bls12_381::{Fr, G1Projective};
253    use ark_ec::{short_weierstrass::SWCurveConfig, CurveGroup};
254    use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial};
255    use ark_std::iter::successors;
256    use jf_utils::test_rng;
257
258    #[test]
259    fn test_poly_eval_single_point() {
260        let mut rng = test_rng();
261        for _ in 0..5 {
262            let degree = rng.gen_range(5..40);
263            let f = GeneralDensePolynomial::<Fr, Fr>::rand(degree, &mut rng);
264            let g = GeneralDensePolynomial::<G1Projective, Fr>::rand(degree, &mut rng);
265            let point = Fr::rand(&mut rng);
266
267            let f_x = f.evaluate(&point);
268            let expected_f_x: Fr =
269                DensePolynomial::from_coefficients_slice(&f.coeffs).evaluate(&point);
270            assert_eq!(f_x, expected_f_x);
271
272            let g_x = g.evaluate(&point);
273            // g(X) = G0 + G1 * X + G2 * X^2 + G3 * X^3 + ...
274            // turn into an MSM between [G0, G1, G2, ...] and [1, x, x^2, x^3, ...]
275            let scalars: Vec<Fr> = successors(Some(Fr::from(1u32)), |&prev| Some(prev * point))
276                .take(g.degree() + 1)
277                .collect();
278            let expected_g_x =
279                SWCurveConfig::msm(&CurveGroup::normalize_batch(&g.coeffs), &scalars).unwrap();
280            assert_eq!(g_x, expected_g_x);
281        }
282    }
283
284    #[test]
285    fn test_poly_add() {
286        let mut rng = test_rng();
287        for _ in 0..5 {
288            let degree = rng.gen_range(5..40);
289            let f_1 = GeneralDensePolynomial::<Fr, Fr>::rand(degree, &mut rng);
290            let mut f_2 = GeneralDensePolynomial::<Fr, Fr>::rand(degree, &mut rng);
291            let expected_f_3 = DensePolynomial::from_coefficients_slice(&f_1.coeffs)
292                + DensePolynomial::from_coefficients_slice(&f_2.coeffs);
293            let f_3 = f_1.clone() + f_2.clone();
294            assert_eq!(f_3.coeffs, expected_f_3.coeffs);
295
296            f_2.coeffs[degree] = -f_1.coeffs[degree];
297            let f_4 = f_1 + f_2;
298            assert_eq!(f_4.degree(), degree - 1,);
299
300            let g_1 = GeneralDensePolynomial::<G1Projective, Fr>::rand(degree, &mut rng);
301            let mut g_2 = GeneralDensePolynomial::<G1Projective, Fr>::rand(degree, &mut rng);
302            let expected_g_3: Vec<G1Projective> = g_1
303                .coeffs
304                .iter()
305                .zip(g_2.coeffs.iter())
306                .map(|(a, b)| a + b)
307                .collect();
308            let g_3 = g_1.clone() + g_2.clone();
309            assert_eq!(g_3.coeffs, expected_g_3);
310
311            g_2.coeffs[degree] = -g_1.coeffs[degree];
312            g_2.coeffs[degree - 1] = -g_1.coeffs[degree - 1];
313            g_2.coeffs[degree - 2] = -g_1.coeffs[degree - 2];
314            let g_4 = g_1 + g_2;
315            assert_eq!(g_4.degree(), degree - 3);
316        }
317    }
318
319    #[test]
320    fn test_multi_open() {
321        let mut rng = test_rng();
322        let degrees = [14, 15, 16, 17, 18];
323
324        for degree in degrees {
325            // TODO: (alex) change to a higher degree when need to test cutoff point and
326            // FFT-based eval on arbitrary points
327            let num_points = rng.gen_range(degree - 4..degree + 4); // should allow more points than degree
328            let mut f = GeneralDensePolynomial::<Fr, Fr>::rand(degree, &mut rng);
329            let mut g = GeneralDensePolynomial::<G1Projective, Fr>::rand(degree, &mut rng);
330
331            let points: Vec<Fr> = (0..num_points).map(|_| Fr::rand(&mut rng)).collect();
332            ark_std::println!("degree: {}, num_points: {}", degree, num_points);
333
334            // First, test general points
335            assert_eq!(
336                f.batch_evaluate(&points),
337                points.iter().map(|x| f.evaluate(x)).collect::<Vec<_>>()
338            );
339            assert_eq!(
340                g.batch_evaluate(&points),
341                points.iter().map(|x| g.evaluate(x)).collect::<Vec<_>>()
342            );
343
344            // Second, test points at roots-of-unity
345            let domain =
346                Radix2EvaluationDomain::new(ark_std::cmp::max(degree + 1, num_points)).unwrap();
347            let roots = domain.elements();
348            assert_eq!(
349                f.batch_evaluate_rou(&domain).unwrap()[..num_points],
350                roots
351                    .take(num_points)
352                    .map(|x| f.evaluate(&x))
353                    .collect::<Vec<_>>()
354            );
355            assert_eq!(
356                g.batch_evaluate_rou(&domain).unwrap()[..num_points],
357                domain
358                    .elements()
359                    .take(num_points)
360                    .map(|x| g.evaluate(&x))
361                    .collect::<Vec<_>>()
362            );
363        }
364    }
365}