mvpoly/
lib.rs

1//! This module contains the definition of the `MVPoly` trait, which is used to
2//! represent multi-variate polynomials.
3//!
4//! Different representations are provided in the sub-modules:
5//! - `monomials`: a representation based on monomials
6//! - `prime`: a representation based on a mapping from variables to prime
7//!    numbers. This representation is unmaintained for now. We leave it
8//!    for interested users.
9//!
10//! "Expressions", as defined in the [kimchi] crate, can be converted into a
11//! multi-variate polynomial using the `from_expr` method.
12
13use ark_ff::PrimeField;
14use kimchi::circuits::expr::{
15    ConstantExpr, ConstantExprInner, ConstantTerm, Expr, ExprInner, Operations, Variable,
16};
17use rand::RngCore;
18use std::collections::HashMap;
19
20pub mod monomials;
21pub mod pbt;
22pub mod prime;
23pub mod utils;
24
25/// Generic trait to represent a multi-variate polynomial
26pub trait MVPoly<F: PrimeField, const N: usize, const D: usize>:
27    // Addition
28    std::ops::Add<Self, Output = Self>
29    + for<'a> std::ops::Add<&'a Self, Output = Self>
30    // Mul
31    + std::ops::Mul<Self, Output = Self>
32    // Negation
33    + std::ops::Neg<Output = Self>
34    // Sub
35    + std::ops::Sub<Self, Output = Self>
36    + for<'a> std::ops::Sub<&'a Self, Output = Self>
37    + ark_ff::One
38    + ark_ff::Zero
39    + std::fmt::Debug
40    + Clone
41    // Comparison operators
42    + PartialEq
43    + Eq
44    // Useful conversions
45    + From<F>
46    + Sized
47{
48    /// Generate a random polynomial of maximum degree `max_degree`.
49    ///
50    /// If `None` is provided as the maximum degree, the polynomial will be
51    /// generated with a maximum degree of `D`.
52    ///
53    /// # Safety
54    ///
55    /// Marked as unsafe to warn the user to use it with caution and to not
56    /// necessarily rely on it for security/randomness in cryptographic
57    /// protocols. The user is responsible for providing its own secure
58    /// polynomial random generator, if needed.
59    ///
60    /// For now, the function is only used for testing.
61    unsafe fn random<RNG: RngCore>(rng: &mut RNG, max_degree: Option<usize>) -> Self;
62
63    fn double(&self) -> Self;
64
65    fn is_constant(&self) -> bool;
66
67    fn mul_by_scalar(&self, scalar: F) -> Self;
68
69    /// Returns the degree of the polynomial.
70    ///
71    /// The degree of the polynomial is the maximum degree of the monomials
72    /// that have a non-zero coefficient.
73    ///
74    /// # Safety
75    ///
76    /// The zero polynomial as a degree equals to 0, as the degree of the
77    /// constant polynomials. We do use the `unsafe` keyword to warn the user
78    /// for this specific case.
79    unsafe fn degree(&self) -> usize;
80
81    /// Evaluate the polynomial at the vector point `x`.
82    ///
83    /// This is a dummy implementation. A cache can be used for the monomials to
84    /// speed up the computation.
85    fn eval(&self, x: &[F; N]) -> F;
86
87    /// Build the univariate polynomial `x_i` from the variable `i`.
88    /// The conversion into the type `usize` is unspecified by this trait. It
89    /// is left to the trait implementation.
90    /// For instance, in the case of [crate::prime], the output must be a prime
91    /// number, starting at `2`. [crate::utils::PrimeNumberGenerator] can be
92    /// used.
93    /// For [crate::monomials], the output must be the index of the variable,
94    /// starting from `0`.
95    ///
96    /// The parameter `offset_next_row` is an optional argument that is used to
97    /// support the case where the "next row" is used. In this case, the type
98    /// parameter `N` must include this offset (i.e. if 4 variables are in ued,
99    /// N should be at least `8 = 2 * 4`).
100    fn from_variable<Column: Into<usize>>(var: Variable<Column>, offset_next_row: Option<usize>) -> Self;
101
102    fn from_constant<ChallengeTerm: Clone>(op: Operations<ConstantExprInner<F, ChallengeTerm>>) -> Self {
103        use kimchi::circuits::expr::Operations::*;
104        match op {
105            Atom(op_const) => {
106                match op_const {
107                    ConstantExprInner::Challenge(_) => {
108                        unimplemented!("Challenges are not supposed to be used in this context for now")
109                    }
110                    ConstantExprInner::Constant(ConstantTerm::EndoCoefficient) => {
111                        unimplemented!(
112                            "The constant EndoCoefficient is not supposed to be used in this context"
113                        )
114                    }
115                    ConstantExprInner::Constant(ConstantTerm::Mds {
116                        row: _row,
117                        col: _col,
118                    }) => {
119                        unimplemented!("The constant Mds is not supposed to be used in this context")
120                    }
121                    ConstantExprInner::Constant(ConstantTerm::Literal(c)) => Self::from(c),
122                }
123            }
124            Add(c1, c2) => Self::from_constant(*c1) + Self::from_constant(*c2),
125            Sub(c1, c2) => Self::from_constant(*c1) - Self::from_constant(*c2),
126            Mul(c1, c2) => Self::from_constant(*c1) * Self::from_constant(*c2),
127            Square(c) => Self::from_constant(*c.clone()) * Self::from_constant(*c),
128            Double(c1) => Self::from_constant(*c1).double(),
129            Pow(c, e) => {
130                // FIXME: dummy implementation
131                let p = Self::from_constant(*c);
132                let mut result = p.clone();
133                for _ in 0..e {
134                    result = result.clone() * p.clone();
135                }
136                result
137            }
138            Cache(_c, _) => {
139                unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
140            }
141            IfFeature(_c, _t, _f) => {
142                unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
143            }
144        }
145    }
146
147    /// Build a value from an expression.
148    /// This method aims to be used to be retro-compatible with what we call
149    /// "the expression framework".
150    /// In the near future, the "expression framework" should be moved also into
151    /// this library.
152    ///
153    /// The mapping from variable to the user is left unspecified by this trait
154    /// and is left to the implementation. The conversion of a variable into an
155    /// index is done by the trait requirement `Into<usize>` on the column type.
156    ///
157    /// The parameter `offset_next_row` is an optional argument that is used to
158    /// support the case where the "next row" is used. In this case, the type
159    /// parameter `N` must include this offset (i.e. if 4 variables are in used,
160    /// N should be at least `8 = 2 * 4`).
161    fn from_expr<Column: Into<usize>, ChallengeTerm: Clone>(expr: Expr<ConstantExpr<F, ChallengeTerm>, Column>, offset_next_row: Option<usize>) -> Self {
162        use kimchi::circuits::expr::Operations::*;
163
164        match expr {
165            Atom(op_const) => {
166                match op_const {
167                    ExprInner::UnnormalizedLagrangeBasis(_) => {
168                        unimplemented!("Not used in this context")
169                    }
170                    ExprInner::VanishesOnZeroKnowledgeAndPreviousRows => {
171                        unimplemented!("Not used in this context")
172                    }
173                    ExprInner::Constant(c) => Self::from_constant(c),
174                    ExprInner::Cell(var) => {
175                        Self::from_variable::<Column>(var, offset_next_row)
176                    }
177                }
178            }
179            Add(e1, e2) => {
180                let p1 = Self::from_expr::<Column, ChallengeTerm>(*e1, offset_next_row);
181                let p2 = Self::from_expr::<Column, ChallengeTerm>(*e2, offset_next_row);
182                p1 + p2
183            }
184            Sub(e1, e2) => {
185                let p1 = Self::from_expr::<Column, ChallengeTerm>(*e1, offset_next_row);
186                let p2 = Self::from_expr::<Column, ChallengeTerm>(*e2, offset_next_row);
187                p1 - p2
188            }
189            Mul(e1, e2) => {
190                let p1 = Self::from_expr::<Column, ChallengeTerm>(*e1, offset_next_row);
191                let p2 = Self::from_expr::<Column, ChallengeTerm>(*e2, offset_next_row);
192                p1 * p2
193            }
194            Double(p) => {
195                let p = Self::from_expr::<Column, ChallengeTerm>(*p, offset_next_row);
196                p.double()
197            }
198            Square(p) => {
199                let p = Self::from_expr::<Column, ChallengeTerm>(*p, offset_next_row);
200                p.clone() * p.clone()
201            }
202            Pow(c, e) => {
203                // FIXME: dummy implementation
204                let p = Self::from_expr::<Column, ChallengeTerm>(*c, offset_next_row);
205                let mut result = p.clone();
206                for _ in 0..e {
207                    result = result.clone() * p.clone();
208                }
209                result
210            }
211            Cache(_c, _) => {
212                unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
213            }
214            IfFeature(_c, _t, _f) => {
215                unimplemented!("The method is supposed to be used for generic multivariate expressions, not tied to a specific use case like Kimchi with this constructor")
216            }
217        }
218    }
219
220    /// Returns true if the polynomial is homogeneous (of degree `D`).
221    /// As a reminder, a polynomial is homogeneous if all its monomials have the
222    /// same degree.
223    fn is_homogeneous(&self) -> bool;
224
225    /// Evaluate the polynomial at the vector point `x` and the extra variable
226    /// `u` using its homogeneous form of degree D.
227    fn homogeneous_eval(&self, x: &[F; N], u: F) -> F;
228
229    /// Add the monomial `coeff * x_1^{e_1} * ... * x_N^{e_N}` to the
230    /// polynomial, where `e_i` are the values given by the array `exponents`.
231    ///
232    /// For instance, to add the monomial `3 * x_1^2 * x_2^3` to the polynomial,
233    /// one would call `add_monomial([2, 3], 3)`.
234    fn add_monomial(&mut self, exponents: [usize; N], coeff: F);
235
236    /// Compute the cross-terms as described in [Behind Nova: cross-terms
237    /// computation for high degree
238    /// gates](https://hackmd.io/@dannywillems/Syo5MBq90)
239    ///
240    /// The polynomial must not necessarily be homogeneous. For this reason, the
241    /// values `u1` and `u2` represents the extra variable that is used to make
242    /// the polynomial homogeneous.
243    ///
244    /// The homogeneous degree is supposed to be the one defined by the type of
245    /// the polynomial, i.e. `D`.
246    ///
247    /// The output is a map of `D - 1` values that represents the cross-terms
248    /// for each power of `r`.
249    fn compute_cross_terms(
250        &self,
251        eval1: &[F; N],
252        eval2: &[F; N],
253        u1: F,
254        u2: F,
255    ) -> HashMap<usize, F>;
256
257    /// Compute the cross-terms of the given polynomial, scaled by the given
258    /// scalar.
259    ///
260    /// More explicitly, given a polynomial `P(X1, ..., Xn)` and a scalar α, the
261    /// method computes the cross-terms of the polynomial `Q(X1, ..., Xn, α)
262    /// = α * P(X1, ..., Xn)`. For this reason, the method takes as input the
263    /// two different scalars `scalar1` and `scalar2` as we are considering the
264    /// scaling factor as a variable.
265    ///
266    /// This method is particularly useful when you need to compute a
267    /// (possibly random) combinaison of polynomials `P1(X1, ..., Xn), ...,
268    /// Pm(X1, ..., Xn)`, like when computing a quotient polynomial in the PlonK
269    /// PIOP, as the result is the sum of individual "scaled" polynomials:
270    /// ```text
271    /// Q(X_1, ..., X_n, α_1, ..., α_m) =
272    ///   α_1 P1(X_1, ..., X_n) +
273    ///   ...
274    ///   α_m Pm(X_1, ..., X_n) +
275    /// ```
276    ///
277    /// The polynomial must not necessarily be homogeneous. For this reason, the
278    /// values `u1` and `u2` represents the extra variable that is used to make
279    /// the polynomial homogeneous.
280    ///
281    /// The homogeneous degree is supposed to be the one defined by the type of
282    /// the polynomial `P`, i.e. `D`.
283    ///
284    /// The output is a map of `D` values that represents the cross-terms
285    /// for each power of `r`.
286    fn compute_cross_terms_scaled(
287        &self,
288        eval1: &[F; N],
289        eval2: &[F; N],
290        u1: F,
291        u2: F,
292        scalar1: F,
293        scalar2: F,
294    ) -> HashMap<usize, F>;
295
296    /// Modify the monomial in the polynomial to the new value `coeff`.
297    fn modify_monomial(&mut self, exponents: [usize; N], coeff: F);
298
299    /// Return true if the multi-variate polynomial is multilinear, i.e. if each
300    /// variable in each monomial is of maximum degree 1.
301    fn is_multilinear(&self) -> bool;
302}
303
304/// Compute the cross terms of a list of polynomials. The polynomials are
305/// linearly combined using the power of a combiner, often called `α`.
306pub fn compute_combined_cross_terms<
307    F: PrimeField,
308    const N: usize,
309    const D: usize,
310    T: MVPoly<F, N, D>,
311>(
312    polys: Vec<T>,
313    eval1: [F; N],
314    eval2: [F; N],
315    u1: F,
316    u2: F,
317    combiner1: F,
318    combiner2: F,
319) -> HashMap<usize, F> {
320    // These should never happen as they should be random
321    // It also makes the code cleaner as we do not need to handle 0^0
322    assert!(combiner1 != F::zero());
323    assert!(combiner2 != F::zero());
324    assert!(u1 != F::zero());
325    assert!(u2 != F::zero());
326    polys
327        .into_iter()
328        .enumerate()
329        .fold(HashMap::new(), |mut acc, (i, poly)| {
330            let scalar1 = combiner1.pow([i as u64]);
331            let scalar2 = combiner2.pow([i as u64]);
332            let res = poly.compute_cross_terms_scaled(&eval1, &eval2, u1, u2, scalar1, scalar2);
333            res.iter().for_each(|(p, r)| {
334                acc.entry(*p).and_modify(|e| *e += r).or_insert(*r);
335            });
336            acc
337        })
338}