commonware_cryptography/bls12381/primitives/
poly.rs

1//! Polynomial operations over the BLS12-381 scalar field.
2//!
3//! # Warning
4//!
5//! The security of the polynomial operations is critical for the overall
6//! security of the threshold schemes. Ensure that the scalar field operations
7//! are performed over the correct field and that all elements are valid.
8
9use super::variant::Variant;
10use crate::bls12381::primitives::{
11    group::{self, Element, Scalar},
12    Error,
13};
14#[cfg(not(feature = "std"))]
15use alloc::collections::BTreeMap;
16#[cfg(not(feature = "std"))]
17use alloc::{vec, vec::Vec};
18use bytes::{Buf, BufMut};
19use commonware_codec::{varint::UInt, EncodeSize, Error as CodecError, Read, ReadExt, Write};
20use core::{hash::Hash, iter};
21#[cfg(feature = "std")]
22use rand::rngs::OsRng;
23use rand_core::CryptoRngCore;
24#[cfg(feature = "std")]
25use std::collections::BTreeMap;
26
27/// Private polynomials are used to generate secret shares.
28pub type Private = Poly<group::Private>;
29
30/// Public polynomials represent commitments to secrets on a private polynomial.
31pub type Public<V> = Poly<<V as Variant>::Public>;
32
33/// Signature polynomials are used in threshold signing (where a signature
34/// is interpolated using at least `threshold` evaluations).
35pub type Signature<V> = Poly<<V as Variant>::Signature>;
36
37/// The partial signature type.
38pub type PartialSignature<V> = Eval<<V as Variant>::Signature>;
39
40/// A polynomial evaluation at a specific index.
41#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct Eval<C: Element> {
43    pub index: u32,
44    pub value: C,
45}
46
47impl<C: Element> Write for Eval<C> {
48    fn write(&self, buf: &mut impl BufMut) {
49        UInt(self.index).write(buf);
50        self.value.write(buf);
51    }
52}
53
54impl<C: Element> Read for Eval<C> {
55    type Cfg = ();
56
57    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
58        let index = UInt::read(buf)?.into();
59        let value = C::read(buf)?;
60        Ok(Self { index, value })
61    }
62}
63
64impl<C: Element> EncodeSize for Eval<C> {
65    fn encode_size(&self) -> usize {
66        UInt(self.index).encode_size() + C::SIZE
67    }
68}
69
70/// A polynomial that is using a scalar for the variable x and a generic
71/// element for the coefficients.
72///
73/// The coefficients must be able to multiply the type of the variable,
74/// which is always a scalar.
75#[derive(Debug, Clone, PartialEq, Eq)]
76// Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L24-L28
77pub struct Poly<C>(Vec<C>);
78
79/// Returns a new scalar polynomial of the given degree where each coefficients is
80/// sampled at random using kernel randomness.
81///
82/// In the context of secret sharing, the threshold is the degree + 1.
83#[cfg(feature = "std")]
84pub fn new(degree: u32) -> Poly<Scalar> {
85    // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L46-L52
86    new_from(degree, &mut OsRng)
87}
88
89// Returns a new scalar polynomial of the given degree where each coefficient is
90// sampled at random from the provided RNG.
91///
92/// In the context of secret sharing, the threshold is the degree + 1.
93pub fn new_from<R: CryptoRngCore>(degree: u32, rng: &mut R) -> Poly<Scalar> {
94    // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L46-L52
95    let coeffs = (0..=degree).map(|_| Scalar::from_rand(rng));
96    Poly::from_iter(coeffs)
97}
98
99/// Returns a new scalar polynomial with a particular value for the constant coefficient.
100///
101/// This does the same thing as [new_from] otherwise.
102pub fn new_with_constant(
103    degree: u32,
104    mut rng: impl CryptoRngCore,
105    constant: Scalar,
106) -> Poly<Scalar> {
107    // (Use skip to avoid an empty range complaint)
108    Poly::from_iter(
109        iter::once(constant).chain((0..=degree).skip(1).map(|_| Scalar::from_rand(&mut rng))),
110    )
111}
112
113/// A Barycentric Weight for interpolation at x=0.
114pub struct Weight(Scalar);
115
116impl Weight {
117    /// Returns the weight as a [Scalar].
118    pub fn as_scalar(&self) -> &Scalar {
119        &self.0
120    }
121}
122
123/// Prepares at least `t` evaluations for Lagrange interpolation.
124pub fn prepare_evaluations<'a, C, I>(threshold: u32, evals: I) -> Result<Vec<&'a Eval<C>>, Error>
125where
126    C: 'a + Element,
127    I: IntoIterator<Item = &'a Eval<C>>,
128{
129    // Check if we have at least `t` evaluations; if not, return an error
130    let t = threshold as usize;
131    let mut evals = evals.into_iter().collect::<Vec<_>>();
132    if evals.len() < t {
133        return Err(Error::NotEnoughPartialSignatures(t, evals.len()));
134    }
135
136    // Convert the first `t` sorted shares into scalars
137    //
138    // We sort the evaluations by index to ensure that two invocations of
139    // `recover` select the same evals.
140    evals.sort_by_key(|e| e.index);
141    evals.truncate(t);
142    Ok(evals)
143}
144
145/// Computes Barycentric Weights for Lagrange interpolation at x=0.
146///
147/// These weights can be reused for multiple interpolations with the same set of points,
148/// which significantly improves performance when recovering a group polynomial or multiple
149/// signatures.
150///
151/// The `indices` of the points used for interpolation (x = index + 1). These indices
152/// should be of length `threshold`, deduped, and sorted.
153pub fn compute_weights(indices: Vec<u32>) -> Result<BTreeMap<u32, Weight>, Error> {
154    // Compute weights for all provided evaluation indices
155    let mut weights = BTreeMap::new();
156    for i in &indices {
157        // Convert i_eval.index to x-coordinate (x = index + 1)
158        let xi = Scalar::from_index(*i);
159
160        // Compute product terms for Lagrange basis polynomial
161        let (mut num, mut den) = (Scalar::one(), Scalar::one());
162        for j in &indices {
163            // Skip if i_eval and j_eval are the same
164            if i == j {
165                continue;
166            }
167
168            // Convert j_eval.index to x-coordinate
169            let xj = Scalar::from_index(*j);
170
171            // Include `xj` in the numerator product for `l_i(0)`
172            num.mul(&xj);
173
174            // Compute `xj - xi` and include it in the denominator product
175            let mut diff = xj;
176            diff.sub(&xi);
177            den.mul(&diff);
178        }
179
180        // Compute the inverse of the denominator product; fails if den is zero (e.g., duplicate `xj`)
181        let inv = den.inverse().ok_or(Error::NoInverse)?;
182
183        // Compute `l_i(0) = num * inv`, the Lagrange basis coefficient at `x=0`
184        num.mul(&inv);
185
186        // Store the weight
187        weights.insert(*i, Weight(num));
188    }
189    Ok(weights)
190}
191
192impl<C> FromIterator<C> for Poly<C> {
193    fn from_iter<T: IntoIterator<Item = C>>(iter: T) -> Self {
194        Self(iter.into_iter().collect())
195    }
196}
197
198impl<C> Poly<C> {
199    /// Creates a new polynomial from the given coefficients.
200    pub fn from(c: Vec<C>) -> Self {
201        Self(c)
202    }
203
204    /// Returns the constant term of the polynomial.
205    pub fn constant(&self) -> &C {
206        &self.0[0]
207    }
208
209    /// Returns the degree of the polynomial
210    pub fn degree(&self) -> u32 {
211        (self.0.len() - 1) as u32 // check size in deserialize, safe to cast
212    }
213
214    /// Returns the number of required shares to reconstruct the polynomial.
215    ///
216    /// This will be the threshold
217    pub fn required(&self) -> u32 {
218        self.0.len() as u32 // check size in deserialize, safe to cast
219    }
220}
221
222impl<C: Element> Poly<C> {
223    /// Commits the scalar polynomial to the group and returns a polynomial over
224    /// the group.
225    ///
226    /// This is done by multiplying each coefficient of the polynomial with the
227    /// group's generator.
228    pub fn commit(commits: Poly<Scalar>) -> Self {
229        // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L322-L340
230        let commits = commits
231            .0
232            .iter()
233            .map(|c| {
234                let mut commitment = C::one();
235                commitment.mul(c);
236                commitment
237            })
238            .collect::<Vec<C>>();
239
240        Poly::<C>::from(commits)
241    }
242
243    /// Returns a zero polynomial.
244    pub fn zero() -> Self {
245        Self::from(vec![C::zero()])
246    }
247
248    /// Returns the given coefficient at the requested index.
249    ///
250    /// It panics if the index is out of range.
251    pub fn get(&self, i: u32) -> C {
252        self.0[i as usize].clone()
253    }
254
255    /// Set the given element at the specified index.
256    ///
257    /// It panics if the index is out of range.
258    pub fn set(&mut self, index: u32, value: C) {
259        self.0[index as usize] = value;
260    }
261
262    /// Performs polynomial addition in place.
263    pub fn add(&mut self, other: &Self) {
264        // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L87-L95
265
266        // if we have a smaller degree we should pad with zeros
267        if self.0.len() < other.0.len() {
268            self.0.resize(other.0.len(), C::zero())
269        }
270
271        self.0.iter_mut().zip(&other.0).for_each(|(a, b)| a.add(b))
272    }
273
274    /// Evaluates the polynomial at the specified index (provided value offset by 1).
275    pub fn evaluate(&self, index: u32) -> Eval<C> {
276        // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L111-L129
277
278        // We add +1 because we must never evaluate the polynomial at its first point
279        // otherwise it reveals the "secret" value after a reshare (where the constant
280        // term is set to be the secret of the previous dealing).
281        let xi = Scalar::from_index(index);
282
283        // Use Horner's method to evaluate the polynomial
284        let value = self.0.iter().rev().fold(C::zero(), |mut sum, coeff| {
285            sum.mul(&xi);
286            sum.add(coeff);
287            sum
288        });
289        Eval { value, index }
290    }
291
292    /// Recovers the constant term of a polynomial of degree less than `t` using `t` evaluations of the polynomial
293    /// and precomputed Barycentric Weights.
294    ///
295    /// This function uses Lagrange interpolation to compute the constant term (i.e., the value of the polynomial at `x=0`)
296    /// given at least `t` distinct evaluations of the polynomial. Each evaluation is assumed to have a unique index,
297    /// which is mapped to a unique x-value as `x = index + 1`.
298    ///
299    /// # References
300    ///
301    /// This implementation is based on [J.-P. Berrut and L. N.
302    /// Trefethen, “Barycentric Lagrange Interpolation,” SIAM Rev., vol. 46, no. 3,
303    /// pp. 501–517, 2004](https://people.maths.ox.ac.uk/trefethen/barycentric.pdf).
304    ///
305    /// # Warning
306    ///
307    /// This function assumes that each evaluation has a unique index. If there are duplicate indices, the function may
308    /// fail with an error when attempting to compute the inverse of zero.
309    pub fn recover_with_weights<'a, I>(
310        weights: &BTreeMap<u32, Weight>,
311        evals: I,
312    ) -> Result<C, Error>
313    where
314        C: 'a,
315        I: IntoIterator<Item = &'a Eval<C>>,
316    {
317        // Scale all evaluations by their corresponding weight
318        let mut result = C::zero();
319        for eval in evals.into_iter() {
320            // Get the weight for the current evaluation index
321            let Some(weight) = weights.get(&eval.index) else {
322                return Err(Error::InvalidIndex);
323            };
324
325            // Scale `yi` by `l_i(0)` to contribute to the constant term
326            let mut scaled_value = eval.value.clone();
327            scaled_value.mul(&weight.0);
328
329            // Add `yi * l_i(0)` to the running sum
330            result.add(&scaled_value);
331        }
332
333        Ok(result)
334    }
335
336    /// Recovers the constant term of a polynomial of degree less than `t` using at least `t` evaluations of
337    /// the polynomial.
338    ///
339    /// This function uses Lagrange interpolation to compute the constant term (i.e., the value of the polynomial at `x=0`)
340    /// given at least `t` distinct evaluations of the polynomial. Each evaluation is assumed to have a unique index,
341    /// which is mapped to a unique x-value as `x = index + 1`.
342    ///
343    /// # References
344    ///
345    /// This implementation is based on [J.-P. Berrut and L. N.
346    /// Trefethen, “Barycentric Lagrange Interpolation,” SIAM Rev., vol. 46, no. 3,
347    /// pp. 501–517, 2004](https://people.maths.ox.ac.uk/trefethen/barycentric.pdf).
348    ///
349    /// # Warning
350    ///
351    /// This function assumes that each evaluation has a unique index. If there are duplicate indices, the function may
352    /// fail with an error when attempting to compute the inverse of zero.
353    pub fn recover<'a, I>(t: u32, evals: I) -> Result<C, Error>
354    where
355        C: 'a,
356        I: IntoIterator<Item = &'a Eval<C>>,
357    {
358        // Prepare evaluations
359        let evals = prepare_evaluations(t, evals)?;
360
361        // Compute weights
362        let indices = evals.iter().map(|e| e.index).collect::<Vec<_>>();
363        let weights = compute_weights(indices)?;
364
365        // Perform interpolation using the precomputed weights
366        Self::recover_with_weights(&weights, evals)
367    }
368}
369
370impl<C: Element> Write for Poly<C> {
371    fn write(&self, buf: &mut impl BufMut) {
372        for c in &self.0 {
373            c.write(buf);
374        }
375    }
376}
377
378impl<C: Element> Read for Poly<C> {
379    type Cfg = usize;
380
381    fn read_cfg(buf: &mut impl Buf, expected: &Self::Cfg) -> Result<Self, CodecError> {
382        let expected_size = C::SIZE * (*expected);
383        if buf.remaining() < expected_size {
384            return Err(CodecError::EndOfBuffer);
385        }
386        let mut coeffs = Vec::with_capacity(*expected);
387        for _ in 0..*expected {
388            coeffs.push(C::read(buf)?);
389        }
390        Ok(Self(coeffs))
391    }
392}
393
394impl<C: Element> EncodeSize for Poly<C> {
395    fn encode_size(&self) -> usize {
396        C::SIZE * self.0.len()
397    }
398}
399
400/// Returns the public key of the polynomial (constant term).
401pub fn public<V: Variant>(public: &Public<V>) -> &V::Public {
402    public.constant()
403}
404
405#[cfg(test)]
406pub mod tests {
407    // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/b0ef82ff79769d085a5a7d3f4fe690b1c8fe6dc9/crates/threshold-bls/src/poly.rs#L355-L604
408    use super::*;
409    use crate::bls12381::primitives::group::{Scalar, G2};
410    use commonware_codec::{Decode, Encode};
411    use rand::SeedableRng;
412    use rand_chacha::ChaCha8Rng;
413
414    #[test]
415    fn poly_degree() {
416        let s = 5;
417        let p = new(s);
418        assert_eq!(p.degree(), s);
419    }
420
421    #[test]
422    fn add_zero() {
423        let p1 = new(3);
424        let p2 = Poly::<Scalar>::zero();
425        let mut res = p1.clone();
426        res.add(&p2);
427        assert_eq!(res, p1);
428
429        let p1 = Poly::<Scalar>::zero();
430        let p2 = new(3);
431        let mut res = p1;
432        res.add(&p2);
433        assert_eq!(res, p2);
434    }
435
436    #[test]
437    fn interpolation_insufficient_shares() {
438        let degree = 4;
439        let threshold = degree + 1;
440        let poly = new(degree);
441        let shares = (0..threshold - 1)
442            .map(|i| poly.evaluate(i))
443            .collect::<Vec<_>>();
444        Poly::recover(threshold, &shares).unwrap_err();
445    }
446
447    #[test]
448    fn evaluate_with_overflow() {
449        let degree = 4;
450        let poly = new(degree);
451        poly.evaluate(u32::MAX);
452    }
453
454    #[test]
455    fn commit() {
456        let secret = new(5);
457        let coeffs = secret.0.clone();
458        let commitment = coeffs
459            .iter()
460            .map(|coeff| {
461                let mut p = G2::one();
462                p.mul(coeff);
463                p
464            })
465            .collect::<Vec<_>>();
466        let commitment = Poly::from(commitment);
467        assert_eq!(commitment, Poly::commit(secret));
468    }
469
470    fn pow(base: Scalar, pow: usize) -> Scalar {
471        let mut res = Scalar::one();
472        for _ in 0..pow {
473            res.mul(&base)
474        }
475        res
476    }
477
478    #[test]
479    fn addition() {
480        for deg1 in 0..100u32 {
481            for deg2 in 0..100u32 {
482                let p1 = new(deg1);
483                let p2 = new(deg2);
484                let mut res = p1.clone();
485                res.add(&p2);
486
487                let (larger, smaller) = if p1.degree() > p2.degree() {
488                    (&p1, &p2)
489                } else {
490                    (&p2, &p1)
491                };
492
493                for i in 0..larger.degree() + 1 {
494                    let i = i as usize;
495                    if i < (smaller.degree() + 1) as usize {
496                        let mut coeff_sum = p1.0[i].clone();
497                        coeff_sum.add(&p2.0[i]);
498                        assert_eq!(res.0[i], coeff_sum);
499                    } else {
500                        assert_eq!(res.0[i], larger.0[i]);
501                    }
502                }
503                assert_eq!(res.degree(), larger.degree(), "deg1={deg1}, deg2={deg2}");
504            }
505        }
506    }
507
508    #[test]
509    fn interpolation() {
510        for degree in 0..100u32 {
511            for num_evals in 0..100u32 {
512                let poly = new(degree);
513                let expected = poly.0[0].clone();
514
515                let shares = (0..num_evals).map(|i| poly.evaluate(i)).collect::<Vec<_>>();
516                let recovered_constant = Poly::recover(num_evals, &shares).unwrap();
517
518                if num_evals > degree {
519                    assert_eq!(
520                        expected, recovered_constant,
521                        "degree={degree}, num_evals={num_evals}"
522                    );
523                } else {
524                    assert_ne!(
525                        expected, recovered_constant,
526                        "degree={degree}, num_evals={num_evals}"
527                    );
528                }
529            }
530        }
531    }
532
533    #[test]
534    fn evaluate() {
535        for d in 0..100u32 {
536            for idx in 0..100_u32 {
537                let x = Scalar::from_index(idx);
538
539                let p1 = new(d);
540                let evaluation = p1.evaluate(idx).value;
541
542                let coeffs = p1.0;
543                let mut sum = coeffs[0].clone();
544                for (i, coeff) in coeffs
545                    .into_iter()
546                    .enumerate()
547                    .take((d + 1) as usize)
548                    .skip(1)
549                {
550                    let xi = pow(x.clone(), i);
551                    let mut var = coeff;
552                    var.mul(&xi);
553                    sum.add(&var);
554                }
555
556                assert_eq!(sum, evaluation, "degree={d}, idx={idx}");
557            }
558        }
559    }
560
561    #[test]
562    fn test_codec() {
563        let original = new(5);
564        let encoded = original.encode();
565        let decoded = Poly::<Scalar>::decode_cfg(encoded, &(original.required() as usize)).unwrap();
566        assert_eq!(original, decoded);
567    }
568
569    #[test]
570    fn test_new_with_constant() {
571        let mut rng = ChaCha8Rng::seed_from_u64(0);
572        let constant = Scalar::from_rand(&mut rng);
573        let poly = new_with_constant(5, &mut rng, constant.clone());
574        assert_eq!(poly.constant(), &constant);
575    }
576}