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