Skip to main content

threshold_pairing/
poly.rs

1//! Utilities for distributed key generation: uni- and bivariate polynomials and commitments.
2//!
3//! If `G` is a group of prime order `r` (written additively), and `g` is a generator, then
4//! multiplication by integers factors through `r`, so the map `x -> x * g` (the sum of `x`
5//! copies of `g`) is a homomorphism from the field `Fr` of integers modulo `r` to `G`. If the
6//! _discrete logarithm_ is hard, i.e. it is infeasible to reverse this map, then `x * g` can be
7//! considered a _commitment_ to `x`: By publishing it, you can guarantee to others that you won't
8//! change your mind about the value `x`, without revealing it.
9//!
10//! This concept extends to polynomials: If you have a polynomial `f` over `Fr`, defined as
11//! `a * X * X + b * X + c`, you can publish `a * g`, `b * g` and `c * g`. Then others will be able
12//! to verify any single value `f(x)` of the polynomial without learning the original polynomial,
13//! because `f(x) * g == x * x * (a * g) + x * (b * g) + (c * g)`. Only after learning three (in
14//! general `degree + 1`) values, they can interpolate `f` itself.
15//!
16//! This module defines univariate polynomials (in one variable) and _symmetric_ bivariate
17//! polynomials (in two variables) over a field `Fr`, as well as their _commitments_ in `G`.
18
19use std::borrow::Borrow;
20use std::cmp::Ordering;
21use std::fmt::{self, Debug, Formatter};
22use std::hash::{Hash, Hasher};
23use std::{cmp, iter, ops};
24
25use core::ops::{AddAssign, Mul, MulAssign, SubAssign};
26use ff::Field;
27use rand::RngCore;
28#[cfg(feature = "serde")]
29use serde::{Deserialize, Serialize};
30use zeroize::{Zeroize, Zeroizing};
31
32use crate::cmp_pairing::cmp_projective;
33use crate::error::{Error, Result};
34use crate::into_fr::IntoScalar;
35use crate::{G1Affine, G1Projective, Scalar};
36
37/// A univariate polynomial in the prime field.
38///
39/// Polynomials are used for secret sharing in threshold cryptography. A polynomial
40/// of degree `t` can be used to create `n` shares such that any `t + 1` shares
41/// can reconstruct the secret (the polynomial's value at 0).
42///
43/// # Example
44///
45/// ```
46/// use threshold_pairing::poly::Poly;
47///
48/// // Create a random polynomial of degree 2
49/// let mut rng = rand::thread_rng();
50/// let poly = Poly::random(2, &mut rng);
51///
52/// // Evaluate the polynomial at different points
53/// let y1 = poly.evaluate(1u64);
54/// let y2 = poly.evaluate(2u64);
55///
56/// // Get the corresponding public commitment
57/// let commitment = poly.commitment();
58/// assert_eq!(poly.degree(), commitment.degree());
59/// ```
60///
61/// # Security
62///
63/// The polynomial coefficients are zeroized on drop to prevent secret leakage.
64#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
65#[derive(PartialEq, Eq, Clone)]
66pub struct Poly {
67    /// The coefficients of a polynomial.
68    #[cfg_attr(feature = "serde", serde(with = "super::serde_impl::field_vec"))]
69    pub(super) coeff: Vec<Scalar>,
70}
71
72impl Zeroize for Poly {
73    fn zeroize(&mut self) {
74        for s in self.coeff.iter_mut() {
75            s.zeroize();
76        }
77    }
78}
79
80impl Drop for Poly {
81    fn drop(&mut self) {
82        self.zeroize();
83    }
84}
85
86/// A debug statement where the `coeff` vector of prime field elements has been redacted.
87impl Debug for Poly {
88    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
89        f.debug_struct("Poly").field("coeff", &"...").finish()
90    }
91}
92
93#[allow(clippy::suspicious_op_assign_impl)]
94impl<B: Borrow<Poly>> ops::AddAssign<B> for Poly {
95    fn add_assign(&mut self, rhs: B) {
96        let len = self.coeff.len();
97        let rhs_len = rhs.borrow().coeff.len();
98        if rhs_len > len {
99            self.coeff.resize(rhs_len, Scalar::zero());
100        }
101        for (self_c, rhs_c) in self.coeff.iter_mut().zip(&rhs.borrow().coeff) {
102            self_c.add_assign(rhs_c);
103        }
104        self.remove_zeros();
105    }
106}
107
108impl<B: Borrow<Poly>> ops::Add<B> for &Poly {
109    type Output = Poly;
110
111    fn add(self, rhs: B) -> Poly {
112        (*self).clone() + rhs
113    }
114}
115
116impl<B: Borrow<Poly>> ops::Add<B> for Poly {
117    type Output = Poly;
118
119    fn add(mut self, rhs: B) -> Poly {
120        self += rhs;
121        self
122    }
123}
124
125impl ops::Add<Scalar> for Poly {
126    type Output = Poly;
127
128    fn add(mut self, rhs: Scalar) -> Self::Output {
129        if self.coeff.is_empty() {
130            if !bool::from(rhs.is_zero()) {
131                self.coeff.push(rhs);
132            }
133        } else {
134            self.coeff[0].add_assign(&rhs);
135            self.remove_zeros();
136        }
137        self
138    }
139}
140
141impl ops::Add<u64> for Poly {
142    type Output = Poly;
143
144    fn add(self, rhs: u64) -> Self::Output {
145        self + rhs.into_scalar()
146    }
147}
148
149impl<B: Borrow<Poly>> ops::SubAssign<B> for Poly {
150    fn sub_assign(&mut self, rhs: B) {
151        let len = self.coeff.len();
152        let rhs_len = rhs.borrow().coeff.len();
153        if rhs_len > len {
154            self.coeff.resize(rhs_len, Scalar::zero());
155        }
156        for (self_c, rhs_c) in self.coeff.iter_mut().zip(&rhs.borrow().coeff) {
157            self_c.sub_assign(rhs_c);
158        }
159        self.remove_zeros();
160    }
161}
162
163impl<B: Borrow<Poly>> ops::Sub<B> for &Poly {
164    type Output = Poly;
165
166    fn sub(self, rhs: B) -> Poly {
167        (*self).clone() - rhs
168    }
169}
170
171impl<B: Borrow<Poly>> ops::Sub<B> for Poly {
172    type Output = Poly;
173
174    fn sub(mut self, rhs: B) -> Poly {
175        self -= rhs;
176        self
177    }
178}
179
180// Clippy thinks using `+` in a `Sub` implementation is suspicious.
181#[allow(clippy::suspicious_arithmetic_impl)]
182impl ops::Sub<Scalar> for Poly {
183    type Output = Poly;
184
185    fn sub(self, rhs: Scalar) -> Self::Output {
186        self + rhs.neg()
187    }
188}
189
190impl ops::Sub<u64> for Poly {
191    type Output = Poly;
192
193    fn sub(self, rhs: u64) -> Self::Output {
194        self - rhs.into_scalar()
195    }
196}
197
198// Clippy thinks using any `+` and `-` in a `Mul` implementation is suspicious.
199#[allow(clippy::suspicious_arithmetic_impl)]
200impl<B: Borrow<Poly>> ops::Mul<B> for &Poly {
201    type Output = Poly;
202
203    fn mul(self, rhs: B) -> Self::Output {
204        let rhs = rhs.borrow();
205        if rhs.is_zero() || self.is_zero() {
206            return Poly::zero();
207        }
208        let n_coeffs = self.coeff.len() + rhs.coeff.len() - 1;
209        let mut coeffs = vec![Scalar::zero(); n_coeffs];
210        let mut tmp = Zeroizing::new(Scalar::zero());
211        for (i, ca) in self.coeff.iter().enumerate() {
212            for (j, cb) in rhs.coeff.iter().enumerate() {
213                *tmp = *ca;
214                tmp.mul_assign(cb);
215                coeffs[i + j].add_assign(&*tmp);
216            }
217        }
218        Poly::from(coeffs)
219    }
220}
221
222impl<B: Borrow<Poly>> ops::Mul<B> for Poly {
223    type Output = Poly;
224
225    fn mul(self, rhs: B) -> Self::Output {
226        &self * rhs
227    }
228}
229
230impl<B: Borrow<Self>> ops::MulAssign<B> for Poly {
231    fn mul_assign(&mut self, rhs: B) {
232        *self = &*self * rhs;
233    }
234}
235
236impl ops::MulAssign<Scalar> for Poly {
237    fn mul_assign(&mut self, rhs: Scalar) {
238        if bool::from(rhs.is_zero()) {
239            self.zeroize();
240            self.coeff.clear();
241        } else {
242            for c in &mut self.coeff {
243                c.mul_assign(rhs);
244            }
245        }
246    }
247}
248
249impl ops::Mul<&Scalar> for Poly {
250    type Output = Poly;
251
252    fn mul(mut self, rhs: &Scalar) -> Self::Output {
253        if bool::from(rhs.is_zero()) {
254            self.zeroize();
255            self.coeff.clear();
256        } else {
257            self.coeff.iter_mut().for_each(|c| c.mul_assign(rhs));
258        }
259        self
260    }
261}
262
263impl ops::Mul<Scalar> for Poly {
264    type Output = Poly;
265
266    fn mul(self, rhs: Scalar) -> Self::Output {
267        let rhs = &rhs;
268        self * rhs
269    }
270}
271
272impl<'a> ops::Mul<&'a Scalar> for &'a Poly {
273    type Output = Poly;
274
275    fn mul(self, rhs: &Scalar) -> Self::Output {
276        (*self).clone() * rhs
277    }
278}
279
280impl ops::Mul<Scalar> for &Poly {
281    type Output = Poly;
282
283    fn mul(self, rhs: Scalar) -> Self::Output {
284        (*self).clone() * rhs
285    }
286}
287
288impl ops::Mul<u64> for Poly {
289    type Output = Poly;
290
291    fn mul(self, rhs: u64) -> Self::Output {
292        self * rhs.into_scalar()
293    }
294}
295
296/// Creates a new `Poly` instance from a vector of prime field elements representing the
297/// coefficients of the polynomial.
298impl From<Vec<Scalar>> for Poly {
299    fn from(coeff: Vec<Scalar>) -> Self {
300        Poly { coeff }
301    }
302}
303
304impl Poly {
305    /// Creates a random polynomial.
306    ///
307    /// # Panics
308    ///
309    /// Panics if the `degree` is too large for the coefficients to fit into a `Vec`.
310    pub fn random<R: RngCore>(degree: usize, rng: &mut R) -> Self {
311        Poly::try_random(degree, rng)
312            .unwrap_or_else(|e| panic!("Failed to create random `Poly`: {}", e))
313    }
314
315    /// Creates a random polynomial. This constructor is identical to the `Poly::random()`
316    /// constructor in every way except that this constructor will return an `Err` where
317    /// `try_random` would return an error.
318    pub fn try_random<R: RngCore>(degree: usize, rng: &mut R) -> Result<Self> {
319        if degree == usize::MAX {
320            return Err(Error::DegreeTooHigh);
321        }
322        let mut coeff = Vec::with_capacity(degree + 1);
323        for _ in 0..=degree {
324            coeff.push(Scalar::random(&mut *rng));
325        }
326        Ok(Poly::from(coeff))
327    }
328
329    /// Returns the polynomial with constant value `0`.
330    pub fn zero() -> Self {
331        Poly { coeff: vec![] }
332    }
333
334    /// Returns `true` if the polynomial is the constant value `0`.
335    pub fn is_zero(&self) -> bool {
336        self.coeff.iter().all(|coeff| bool::from(coeff.is_zero()))
337    }
338
339    /// Returns the polynomial with constant value `1`.
340    pub fn one() -> Self {
341        Poly::constant(Scalar::one())
342    }
343
344    /// Returns the polynomial with constant value `c`.
345    pub fn constant(c: Scalar) -> Self {
346        // c will zeroized on drop because Scalar implements `DefaultsIsZero`.
347        Poly::from(vec![c])
348    }
349
350    /// Returns the identity function, i.e. the polynomial "`x`".
351    pub fn identity() -> Self {
352        Poly::monomial(1)
353    }
354
355    /// Returns the (monic) monomial: `x.pow(degree)`.
356    pub fn monomial(degree: usize) -> Self {
357        let coeff: Vec<Scalar> = iter::repeat_n(Scalar::zero(), degree)
358            .chain(iter::once(Scalar::one()))
359            .collect();
360        Poly::from(coeff)
361    }
362
363    /// Returns the unique polynomial `f` of degree `samples.len() - 1` with the given values
364    /// `(x, f(x))`.
365    pub fn interpolate<T, U, I>(samples_repr: I) -> Self
366    where
367        I: IntoIterator<Item = (T, U)>,
368        T: IntoScalar,
369        U: IntoScalar,
370    {
371        let convert = |(x, y): (T, U)| (x.into_scalar(), y.into_scalar());
372        let samples: Vec<(Scalar, Scalar)> = samples_repr.into_iter().map(convert).collect();
373        Poly::compute_interpolation(&samples)
374    }
375
376    /// Returns the degree.
377    pub fn degree(&self) -> usize {
378        self.coeff.len().saturating_sub(1)
379    }
380
381    /// Returns the value at the point `i`.
382    pub fn evaluate<T: IntoScalar>(&self, i: T) -> Scalar {
383        let mut result = Zeroizing::new(match self.coeff.last() {
384            None => return Scalar::zero(),
385            Some(c) => *c,
386        });
387        let x = Zeroizing::new(i.into_scalar());
388        for c in self.coeff.iter().rev().skip(1) {
389            result.mul_assign(&*x);
390            result.add_assign(c);
391        }
392        *result
393    }
394
395    /// Returns the corresponding commitment.
396    pub fn commitment(&self) -> Commitment {
397        let to_g1 = |c: &Scalar| G1Affine::generator().mul(*c);
398        Commitment {
399            coeff: self.coeff.iter().map(to_g1).collect(),
400        }
401    }
402
403    /// Removes all trailing zero coefficients.
404    fn remove_zeros(&mut self) {
405        let zeros = self
406            .coeff
407            .iter()
408            .rev()
409            .take_while(|c| bool::from(c.is_zero()))
410            .count();
411        let len = self.coeff.len() - zeros;
412        self.coeff.truncate(len);
413    }
414
415    /// Returns the unique polynomial `f` of degree `samples.len() - 1` with the given values
416    /// `(x, f(x))`.
417    fn compute_interpolation(samples: &[(Scalar, Scalar)]) -> Self {
418        if samples.is_empty() {
419            return Poly::zero();
420        }
421        // Interpolates on the first `i` samples.
422        let mut poly = Poly::constant(samples[0].1);
423        let minus_s0 = samples[0].0.neg();
424        // Is zero on the first `i` samples.
425        let mut base = Poly::from(vec![minus_s0, Scalar::one()]);
426
427        // We update `base` so that it is always zero on all previous samples, and `poly` so that
428        // it has the correct values on the previous samples.
429        for (ref x, ref y) in &samples[1..] {
430            // Scale `base` so that its value at `x` is the difference between `y` and `poly`'s
431            // current value at `x`: Adding it to `poly` will then make it correct for `x`.
432            let mut diff = *y;
433            diff.sub_assign(&poly.evaluate(x));
434            let base_val = base.evaluate(x);
435            diff.mul_assign(&base_val.invert().expect("sample points must be distinct"));
436            base *= diff;
437            poly += &base;
438
439            // Finally, multiply `base` by X - x, so that it is zero at `x`, too, now.
440            let minus_x = (*x).neg();
441            base *= Poly::from(vec![minus_x, Scalar::one()]);
442        }
443        poly
444    }
445
446    /// Generates a non-redacted debug string. This method differs from
447    /// the `Debug` implementation in that it *does* leak the secret prime
448    /// field elements.
449    ///
450    /// # Security
451    ///
452    /// This method is intended for debugging purposes only. The output
453    /// contains sensitive polynomial coefficients and should never be logged
454    /// in production or exposed to untrusted parties.
455    ///
456    /// Requires the `expose-secret` feature flag to be enabled. This forces
457    /// an explicit opt-in and prevents accidental inclusion in production builds.
458    #[cfg(feature = "expose-secret")]
459    pub fn reveal(&self) -> String {
460        format!("Poly {{ coeff: {:?} }}", self.coeff)
461    }
462}
463
464/// A commitment to a univariate polynomial.
465///
466/// A commitment allows verification of polynomial evaluations without revealing
467/// the polynomial itself. Given a commitment to polynomial `f`, anyone can verify
468/// that a claimed value `y` equals `f(x)` for a given `x`, without learning `f`.
469///
470/// # Example
471///
472/// ```
473/// use threshold_pairing::poly::Poly;
474///
475/// let mut rng = rand::thread_rng();
476/// let poly = Poly::random(2, &mut rng);
477/// let commitment = poly.commitment();
478///
479/// // The commitment can verify polynomial evaluations
480/// // (in the group, not the field directly)
481/// let x = 5u64;
482/// let y_in_group = commitment.evaluate(x);
483///
484/// // The degree matches the polynomial
485/// assert_eq!(poly.degree(), commitment.degree());
486/// ```
487#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct Commitment {
490    /// The coefficients of the polynomial.
491    #[cfg_attr(feature = "serde", serde(with = "super::serde_impl::projective_vec"))]
492    pub(super) coeff: Vec<G1Projective>,
493}
494
495impl PartialOrd for Commitment {
496    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
497        Some(self.cmp(other))
498    }
499}
500
501impl Ord for Commitment {
502    fn cmp(&self, other: &Self) -> Ordering {
503        self.coeff.len().cmp(&other.coeff.len()).then_with(|| {
504            self.coeff
505                .iter()
506                .zip(&other.coeff)
507                .find(|(x, y)| x != y)
508                .map_or(Ordering::Equal, |(x, y)| cmp_projective(x, y))
509        })
510    }
511}
512
513impl Hash for Commitment {
514    fn hash<H: Hasher>(&self, state: &mut H) {
515        self.coeff.len().hash(state);
516        for c in &self.coeff {
517            G1Affine::from(c).to_compressed().as_ref().hash(state);
518        }
519    }
520}
521
522impl<B: Borrow<Commitment>> ops::AddAssign<B> for Commitment {
523    fn add_assign(&mut self, rhs: B) {
524        let len = cmp::max(self.coeff.len(), rhs.borrow().coeff.len());
525        self.coeff.resize(len, G1Projective::identity());
526        for (self_c, rhs_c) in self.coeff.iter_mut().zip(&rhs.borrow().coeff) {
527            self_c.add_assign(rhs_c);
528        }
529        self.remove_zeros();
530    }
531}
532
533impl<B: Borrow<Commitment>> ops::Add<B> for &Commitment {
534    type Output = Commitment;
535
536    fn add(self, rhs: B) -> Commitment {
537        (*self).clone() + rhs
538    }
539}
540
541impl<B: Borrow<Commitment>> ops::Add<B> for Commitment {
542    type Output = Commitment;
543
544    fn add(mut self, rhs: B) -> Commitment {
545        self += rhs;
546        self
547    }
548}
549
550impl Commitment {
551    /// Returns the polynomial's degree.
552    ///
553    /// Returns 0 for an empty commitment (zero polynomial).
554    pub fn degree(&self) -> usize {
555        self.coeff.len().saturating_sub(1)
556    }
557
558    /// Returns the `i`-th public key share.
559    pub fn evaluate<T: IntoScalar>(&self, i: T) -> G1Projective {
560        let mut result = match self.coeff.last() {
561            None => return G1Projective::identity(),
562            Some(c) => *c,
563        };
564        let x = i.into_scalar();
565        for c in self.coeff.iter().rev().skip(1) {
566            result.mul_assign(x);
567            result.add_assign(c);
568        }
569        result
570    }
571
572    /// Removes all trailing zero coefficients.
573    fn remove_zeros(&mut self) {
574        let zeros = self
575            .coeff
576            .iter()
577            .rev()
578            .take_while(|c| bool::from(c.is_identity()))
579            .count();
580        let len = self.coeff.len() - zeros;
581        self.coeff.truncate(len)
582    }
583}
584
585/// A symmetric bivariate polynomial in the prime field.
586///
587/// This can be used for Verifiable Secret Sharing and Distributed Key Generation. See the module
588/// documentation for details.
589///
590/// A bivariate polynomial `f(x, y)` is symmetric if `f(x, y) = f(y, x)` for all `x` and `y`.
591/// This property is essential for distributed key generation protocols.
592///
593/// # Example
594///
595/// ```
596/// use threshold_pairing::poly::BivarPoly;
597///
598/// let mut rng = rand::thread_rng();
599/// let bi_poly = BivarPoly::random(2, &mut rng);
600///
601/// // The polynomial is symmetric
602/// let val_xy = bi_poly.evaluate(3u64, 5u64);
603/// let val_yx = bi_poly.evaluate(5u64, 3u64);
604/// assert_eq!(val_xy, val_yx);
605///
606/// // Extract a row as a univariate polynomial
607/// let row = bi_poly.row(1u64);
608/// assert_eq!(row.degree(), bi_poly.degree());
609///
610/// // Get the public commitment
611/// let commitment = bi_poly.commitment();
612/// ```
613///
614/// # Security
615///
616/// The polynomial coefficients are zeroized on drop to prevent secret leakage.
617#[derive(Clone)]
618pub struct BivarPoly {
619    /// The polynomial's degree in each of the two variables.
620    degree: usize,
621    /// The coefficients of the polynomial. Coefficient `(i, j)` for `i <= j` is in position
622    /// `j * (j + 1) / 2 + i`.
623    coeff: Vec<Scalar>,
624}
625
626impl Zeroize for BivarPoly {
627    fn zeroize(&mut self) {
628        for s in self.coeff.iter_mut() {
629            s.zeroize();
630        }
631        self.degree.zeroize();
632    }
633}
634
635impl Drop for BivarPoly {
636    fn drop(&mut self) {
637        self.zeroize();
638    }
639}
640
641/// A debug statement where the `coeff` vector has been redacted.
642impl Debug for BivarPoly {
643    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
644        f.debug_struct("BivarPoly")
645            .field("degree", &self.degree)
646            .field("coeff", &"...")
647            .finish()
648    }
649}
650
651impl BivarPoly {
652    /// Creates a random polynomial.
653    ///
654    /// # Panics
655    ///
656    /// Panics if the degree is too high for the coefficients to fit into a `Vec`.
657    pub fn random<R: RngCore>(degree: usize, rng: &mut R) -> Self {
658        BivarPoly::try_random(degree, rng).unwrap_or_else(|e| {
659            panic!(
660                "Failed to create random `BivarPoly` of degree {}: {}",
661                degree, e
662            )
663        })
664    }
665
666    /// Creates a random polynomial.
667    pub fn try_random<R: RngCore>(degree: usize, rng: &mut R) -> Result<Self> {
668        let len = coeff_pos(degree, degree)
669            .and_then(|l| l.checked_add(1))
670            .ok_or(Error::DegreeTooHigh)?;
671        let mut coeff = Vec::with_capacity(len);
672        for _ in 0..=len - 1 {
673            coeff.push(Scalar::random(&mut *rng));
674        }
675        let poly = BivarPoly { degree, coeff };
676        Ok(poly)
677    }
678
679    /// Returns the polynomial's degree; which is the same in both variables.
680    pub fn degree(&self) -> usize {
681        self.degree
682    }
683
684    /// Returns the polynomial's value at the point `(x, y)`.
685    pub fn evaluate<T: IntoScalar>(&self, x: T, y: T) -> Scalar {
686        let x_pow = self.powers(x);
687        let y_pow = self.powers(y);
688        let mut result = Zeroizing::new(Scalar::zero());
689        for (i, x_pow_i) in x_pow.into_iter().enumerate() {
690            for (j, y_pow_j) in y_pow.iter().enumerate() {
691                let index = coeff_pos(i, j).expect("polynomial degree too high");
692                let mut summand = Zeroizing::new(self.coeff[index]);
693                summand.mul_assign(&x_pow_i);
694                summand.mul_assign(y_pow_j);
695                result.add_assign(&*summand);
696            }
697        }
698        *result
699    }
700
701    /// Returns the `x`-th row, as a univariate polynomial.
702    pub fn row<T: IntoScalar>(&self, x: T) -> Poly {
703        let x_pow = self.powers(x);
704        let coeff: Vec<Scalar> = (0..=self.degree)
705            .map(|i| {
706                let mut result = Zeroizing::new(Scalar::zero());
707                for (j, x_pow_j) in x_pow.iter().enumerate() {
708                    let index = coeff_pos(i, j).expect("polynomial degree too high");
709                    let mut summand = Zeroizing::new(self.coeff[index]);
710                    summand.mul_assign(x_pow_j);
711                    result.add_assign(&*summand);
712                }
713                *result
714            })
715            .collect();
716        Poly::from(coeff)
717    }
718
719    /// Returns the corresponding commitment. That information can be shared publicly.
720    pub fn commitment(&self) -> BivarCommitment {
721        let to_pub = |c: &Scalar| G1Affine::generator().mul(*c);
722        BivarCommitment {
723            degree: self.degree,
724            coeff: self.coeff.iter().map(to_pub).collect(),
725        }
726    }
727
728    /// Returns the `0`-th to `degree`-th power of `x`.
729    fn powers<T: IntoScalar>(&self, x: T) -> Vec<Scalar> {
730        powers(x, self.degree)
731    }
732
733    /// Generates a non-redacted debug string. This method differs from the
734    /// `Debug` implementation in that it *does* leak the struct's
735    /// internal state.
736    ///
737    /// # Security
738    ///
739    /// This method is intended for debugging purposes only. The output
740    /// contains sensitive polynomial coefficients and should never be logged
741    /// in production or exposed to untrusted parties.
742    ///
743    /// Requires the `expose-secret` feature flag to be enabled. This forces
744    /// an explicit opt-in and prevents accidental inclusion in production builds.
745    #[cfg(feature = "expose-secret")]
746    pub fn reveal(&self) -> String {
747        format!(
748            "BivarPoly {{ degree: {}, coeff: {:?} }}",
749            self.degree, self.coeff
750        )
751    }
752}
753
754/// A commitment to a symmetric bivariate polynomial.
755///
756/// This commitment allows verification of values from a [`BivarPoly`] without
757/// revealing the polynomial itself. It is used in distributed key generation
758/// to publicly verify shares.
759///
760/// # Example
761///
762/// ```
763/// use threshold_pairing::poly::BivarPoly;
764///
765/// let mut rng = rand::thread_rng();
766/// let bi_poly = BivarPoly::random(2, &mut rng);
767/// let commitment = bi_poly.commitment();
768///
769/// // The commitment has the same degree as the polynomial
770/// assert_eq!(bi_poly.degree(), commitment.degree());
771///
772/// // Extract a row commitment
773/// let row_commit = commitment.row(1u64);
774/// ```
775#[derive(Debug, Clone, Eq, PartialEq)]
776pub struct BivarCommitment {
777    /// The polynomial's degree in each of the two variables.
778    pub(crate) degree: usize,
779    /// The commitments to the coefficients.
780    pub(crate) coeff: Vec<G1Projective>,
781}
782
783impl Hash for BivarCommitment {
784    fn hash<H: Hasher>(&self, state: &mut H) {
785        self.degree.hash(state);
786        for c in &self.coeff {
787            G1Affine::from(c).to_compressed().as_ref().hash(state);
788        }
789    }
790}
791
792impl PartialOrd for BivarCommitment {
793    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
794        Some(self.cmp(other))
795    }
796}
797
798impl Ord for BivarCommitment {
799    fn cmp(&self, other: &Self) -> Ordering {
800        self.degree.cmp(&other.degree).then_with(|| {
801            self.coeff
802                .iter()
803                .zip(&other.coeff)
804                .find(|(x, y)| x != y)
805                .map_or(Ordering::Equal, |(x, y)| cmp_projective(x, y))
806        })
807    }
808}
809
810impl BivarCommitment {
811    /// Returns the polynomial's degree: It is the same in both variables.
812    pub fn degree(&self) -> usize {
813        self.degree
814    }
815
816    /// Returns the commitment's value at the point `(x, y)`.
817    pub fn evaluate<T: IntoScalar>(&self, x: T, y: T) -> G1Projective {
818        let x_pow = self.powers(x);
819        let y_pow = self.powers(y);
820        let mut result = G1Projective::identity();
821        for (i, x_pow_i) in x_pow.into_iter().enumerate() {
822            for (j, y_pow_j) in y_pow.iter().enumerate() {
823                let index = coeff_pos(i, j).expect("polynomial degree too high");
824                let mut summand = self.coeff[index];
825                summand.mul_assign(x_pow_i);
826                summand.mul_assign(*y_pow_j);
827                result.add_assign(&summand);
828            }
829        }
830        result
831    }
832
833    /// Returns the `x`-th row, as a commitment to a univariate polynomial.
834    pub fn row<T: IntoScalar>(&self, x: T) -> Commitment {
835        let x_pow = self.powers(x);
836        let coeff: Vec<G1Projective> = (0..=self.degree)
837            .map(|i| {
838                let mut result = G1Projective::identity();
839                for (j, x_pow_j) in x_pow.iter().enumerate() {
840                    let index = coeff_pos(i, j).expect("polynomial degree too high");
841                    let mut summand = self.coeff[index];
842                    summand.mul_assign(*x_pow_j);
843                    result.add_assign(&summand);
844                }
845                result
846            })
847            .collect();
848        Commitment { coeff }
849    }
850
851    /// Returns the `0`-th to `degree`-th power of `x`.
852    fn powers<T: IntoScalar>(&self, x: T) -> Vec<Scalar> {
853        powers(x, self.degree)
854    }
855}
856
857/// Returns the `0`-th to `degree`-th power of `x`.
858fn powers<T: IntoScalar>(into_x: T, degree: usize) -> Vec<Scalar> {
859    let x = Zeroizing::new(into_x.into_scalar());
860    let mut x_pow_i = Zeroizing::new(Scalar::one());
861    iter::once(*x_pow_i)
862        .chain((0..degree).map(|_| {
863            x_pow_i.mul_assign(&*x);
864            *x_pow_i
865        }))
866        .collect()
867}
868
869/// Returns the position of coefficient `(i, j)` in the vector describing a symmetric bivariate
870/// polynomial. If `i` or `j` are too large to represent the position as a `usize`, `None` is
871/// returned.
872pub(crate) fn coeff_pos(i: usize, j: usize) -> Option<usize> {
873    // Since the polynomial is symmetric, we can order such that `j >= i`.
874    let (j, i) = if j >= i { (j, i) } else { (i, j) };
875    i.checked_add(j.checked_mul(j.checked_add(1)?)? / 2)
876}
877
878#[cfg(test)]
879mod tests {
880    use core::ops::{AddAssign, Mul};
881    use std::collections::BTreeMap;
882
883    use super::{coeff_pos, BivarPoly, IntoScalar, Poly};
884    use super::{G1Affine, G1Projective, Scalar};
885    use ff::Field;
886    use zeroize::Zeroize;
887
888    #[test]
889    fn test_coeff_pos() {
890        let mut i = 0;
891        let mut j = 0;
892        for n in 0..100 {
893            assert_eq!(Some(n), coeff_pos(i, j));
894            if i >= j {
895                j += 1;
896                i = 0;
897            } else {
898                i += 1;
899            }
900        }
901        let too_large = 1 << (0usize.count_zeros() / 2);
902        assert_eq!(None, coeff_pos(0, too_large));
903    }
904
905    #[test]
906    fn test_commitment_degree_empty_does_not_panic() {
907        use super::Commitment;
908        // An empty commitment (zero polynomial) should not panic
909        let empty_commitment = Commitment { coeff: vec![] };
910        // degree() should return 0 for empty coefficient vectors (not panic)
911        assert_eq!(empty_commitment.degree(), 0);
912    }
913
914    #[test]
915    fn test_poly_degree_empty() {
916        // A zero polynomial has degree 0
917        let zero_poly = Poly::zero();
918        assert_eq!(zero_poly.degree(), 0);
919        // Its commitment should also have degree 0
920        let commitment = zero_poly.commitment();
921        assert_eq!(commitment.degree(), 0);
922    }
923
924    #[test]
925    fn poly() {
926        // The polynomial 5 X³ + X - 2.
927        let x_pow_3 = Poly::monomial(3);
928        let x_pow_1 = Poly::monomial(1);
929        let poly = x_pow_3 * 5 + x_pow_1 - 2;
930
931        let coeff: Vec<_> = [-2, 1, 0, 5].iter().map(IntoScalar::into_scalar).collect();
932        assert_eq!(Poly { coeff }, poly);
933        let samples = vec![(-1, -8), (2, 40), (3, 136), (5, 628)];
934        for &(x, y) in &samples {
935            assert_eq!(y.into_scalar(), poly.evaluate(x));
936        }
937        let interp = Poly::interpolate(samples);
938        assert_eq!(interp, poly);
939    }
940
941    #[test]
942    fn test_zeroize() {
943        let mut poly = Poly::monomial(3) + Poly::monomial(2) - 1;
944        poly.zeroize();
945        assert!(poly.is_zero());
946
947        let mut bi_poly = BivarPoly::random(3, &mut rand::thread_rng());
948        let random_commitment = bi_poly.commitment();
949
950        bi_poly.zeroize();
951
952        let zero_commitment = bi_poly.commitment();
953        assert_ne!(random_commitment, zero_commitment);
954
955        let mut rng = rand::thread_rng();
956        let (x, y): (Scalar, Scalar) = (Scalar::random(&mut rng), Scalar::random(&mut rng));
957        assert_eq!(zero_commitment.evaluate(x, y), G1Projective::identity());
958    }
959
960    #[test]
961    fn distributed_key_generation() {
962        let mut rng = rand::thread_rng();
963        let dealer_num = 3;
964        let node_num = 5;
965        let faulty_num = 2;
966
967        // For distributed key generation, a number of dealers, only one of who needs to be honest,
968        // generates random bivariate polynomials and publicly commits to them. In practice, the
969        // dealers can e.g. be any `faulty_num + 1` nodes.
970        let bi_polys: Vec<BivarPoly> = (0..dealer_num)
971            .map(|_| BivarPoly::random(faulty_num, &mut rng))
972            .collect();
973        let pub_bi_commits: Vec<_> = bi_polys.iter().map(BivarPoly::commitment).collect();
974
975        let mut sec_keys = vec![Scalar::zero(); node_num];
976
977        // Each dealer sends row `m` to node `m`, where the index starts at `1`. Don't send row `0`
978        // to anyone! The nodes verify their rows, and send _value_ `s` on to node `s`. They again
979        // verify the values they received, and collect them.
980        for (bi_poly, bi_commit) in bi_polys.iter().zip(&pub_bi_commits) {
981            for m in 1..=node_num {
982                // Node `m` receives its row and verifies it.
983                let row_poly = bi_poly.row(m);
984                let row_commit = bi_commit.row(m);
985                assert_eq!(row_poly.commitment(), row_commit);
986                // Node `s` receives the `s`-th value and verifies it.
987                for s in 1..=node_num {
988                    let val = row_poly.evaluate(s);
989                    let val_g1 = G1Affine::generator().mul(val);
990                    assert_eq!(bi_commit.evaluate(m, s), val_g1);
991                    // The node can't verify this directly, but it should have the correct value:
992                    assert_eq!(bi_poly.evaluate(m, s), val);
993                }
994
995                // A cheating dealer who modified the polynomial would be detected.
996                let x_pow_2 = Poly::monomial(2);
997                let five = Poly::constant(5.into_scalar());
998                let wrong_poly = row_poly.clone() + x_pow_2 * five;
999                assert_ne!(wrong_poly.commitment(), row_commit);
1000
1001                // If `2 * faulty_num + 1` nodes confirm that they received a valid row, then at
1002                // least `faulty_num + 1` honest ones did, and sent the correct values on to node
1003                // `s`. So every node received at least `faulty_num + 1` correct entries of their
1004                // column/row (remember that the bivariate polynomial is symmetric). They can
1005                // reconstruct the full row and in particular value `0` (which no other node knows,
1006                // only the dealer). E.g. let's say nodes `1`, `2` and `4` are honest. Then node
1007                // `m` received three correct entries from that row:
1008                let received: BTreeMap<_, _> = [1, 2, 4]
1009                    .iter()
1010                    .map(|&i| (i, bi_poly.evaluate(m, i)))
1011                    .collect();
1012                let my_row = Poly::interpolate(received);
1013                assert_eq!(bi_poly.evaluate(m, 0), my_row.evaluate(0));
1014                assert_eq!(row_poly, my_row);
1015
1016                // The node sums up all values number `0` it received from the different dealer. No
1017                // dealer and no other node knows the sum in the end.
1018                sec_keys[m - 1].add_assign(&my_row.evaluate(Scalar::zero()));
1019            }
1020        }
1021
1022        // Each node now adds up all the first values of the rows it received from the different
1023        // dealers (excluding the dealers where fewer than `2 * faulty_num + 1` nodes confirmed).
1024        // The whole first column never gets added up in practice, because nobody has all the
1025        // information. We do it anyway here; entry `0` is the secret key that is not known to
1026        // anyone, neither a dealer, nor a node:
1027        let mut sec_key_set = Poly::zero();
1028        for bi_poly in &bi_polys {
1029            sec_key_set += bi_poly.row(0);
1030        }
1031        for m in 1..=node_num {
1032            assert_eq!(sec_key_set.evaluate(m), sec_keys[m - 1]);
1033        }
1034
1035        // The sum of the first rows of the public commitments is the commitment to the secret key
1036        // set.
1037        let mut sum_commit = Poly::zero().commitment();
1038        for bi_commit in &pub_bi_commits {
1039            sum_commit += bi_commit.row(0);
1040        }
1041        assert_eq!(sum_commit, sec_key_set.commitment());
1042    }
1043}