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)
87        .map(|_| Scalar::from_rand(rng))
88        .collect::<Vec<_>>();
89    Poly::<Scalar>(coeffs)
90}
91
92/// A Barycentric Weight for interpolation at x=0.
93pub struct Weight(Scalar);
94
95impl Weight {
96    /// Returns the weight as a [Scalar].
97    pub fn as_scalar(&self) -> &Scalar {
98        &self.0
99    }
100}
101
102/// Prepares at least `t` evaluations for Lagrange interpolation.
103pub fn prepare_evaluations<'a, C, I>(threshold: u32, evals: I) -> Result<Vec<&'a Eval<C>>, Error>
104where
105    C: 'a + Element,
106    I: IntoIterator<Item = &'a Eval<C>>,
107{
108    // Check if we have at least `t` evaluations; if not, return an error
109    let t = threshold as usize;
110    let mut evals = evals.into_iter().collect::<Vec<_>>();
111    if evals.len() < t {
112        return Err(Error::NotEnoughPartialSignatures(t, evals.len()));
113    }
114
115    // Convert the first `t` sorted shares into scalars
116    //
117    // We sort the evaluations by index to ensure that two invocations of
118    // `recover` select the same evals.
119    evals.sort_by_key(|e| e.index);
120    evals.truncate(t);
121    Ok(evals)
122}
123
124/// Computes Barycentric Weights for Lagrange interpolation at x=0.
125///
126/// These weights can be reused for multiple interpolations with the same set of points,
127/// which significantly improves performance when recovering a group polynomial or multiple
128/// signatures.
129///
130/// The `indices` of the points used for interpolation (x = index + 1). These indices
131/// should be of length `threshold`, deduped, and sorted.
132pub fn compute_weights(indices: Vec<u32>) -> Result<BTreeMap<u32, Weight>, Error> {
133    // Compute weights for all provided evaluation indices
134    let mut weights = BTreeMap::new();
135    for i in &indices {
136        // Convert i_eval.index to x-coordinate (x = index + 1)
137        let xi = Scalar::from_index(*i);
138
139        // Compute product terms for Lagrange basis polynomial
140        let (mut num, mut den) = (Scalar::one(), Scalar::one());
141        for j in &indices {
142            // Skip if i_eval and j_eval are the same
143            if i == j {
144                continue;
145            }
146
147            // Convert j_eval.index to x-coordinate
148            let xj = Scalar::from_index(*j);
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 index (provided value offset by 1).
248    pub fn evaluate(&self, index: 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 xi = Scalar::from_index(index);
255
256        // Use Horner's method to evaluate the polynomial
257        let value = self.0.iter().rev().fold(C::zero(), |mut sum, coeff| {
258            sum.mul(&xi);
259            sum.add(coeff);
260            sum
261        });
262        Eval { value, index }
263    }
264
265    /// Recovers the constant term of a polynomial of degree less than `t` using `t` evaluations of the polynomial
266    /// and precomputed Barycentric Weights.
267    ///
268    /// This function uses Lagrange interpolation to compute the constant term (i.e., the value of the polynomial at `x=0`)
269    /// given at least `t` distinct evaluations of the polynomial. Each evaluation is assumed to have a unique index,
270    /// which is mapped to a unique x-value as `x = index + 1`.
271    ///
272    /// # References
273    ///
274    /// This implementation is based on [J.-P. Berrut and L. N.
275    /// Trefethen, “Barycentric Lagrange Interpolation,” SIAM Rev., vol. 46, no. 3,
276    /// pp. 501–517, 2004](https://people.maths.ox.ac.uk/trefethen/barycentric.pdf).
277    ///
278    /// # Warning
279    ///
280    /// This function assumes that each evaluation has a unique index. If there are duplicate indices, the function may
281    /// fail with an error when attempting to compute the inverse of zero.
282    pub fn recover_with_weights<'a, I>(
283        weights: &BTreeMap<u32, Weight>,
284        evals: I,
285    ) -> Result<C, Error>
286    where
287        C: 'a,
288        I: IntoIterator<Item = &'a Eval<C>>,
289    {
290        // Scale all evaluations by their corresponding weight
291        let mut result = C::zero();
292        for eval in evals.into_iter() {
293            // Get the weight for the current evaluation index
294            let Some(weight) = weights.get(&eval.index) else {
295                return Err(Error::InvalidIndex);
296            };
297
298            // Scale `yi` by `l_i(0)` to contribute to the constant term
299            let mut scaled_value = eval.value.clone();
300            scaled_value.mul(&weight.0);
301
302            // Add `yi * l_i(0)` to the running sum
303            result.add(&scaled_value);
304        }
305
306        Ok(result)
307    }
308
309    /// Recovers the constant term of a polynomial of degree less than `t` using at least `t` evaluations of
310    /// the polynomial.
311    ///
312    /// This function uses Lagrange interpolation to compute the constant term (i.e., the value of the polynomial at `x=0`)
313    /// given at least `t` distinct evaluations of the polynomial. Each evaluation is assumed to have a unique index,
314    /// which is mapped to a unique x-value as `x = index + 1`.
315    ///
316    /// # References
317    ///
318    /// This implementation is based on [J.-P. Berrut and L. N.
319    /// Trefethen, “Barycentric Lagrange Interpolation,” SIAM Rev., vol. 46, no. 3,
320    /// pp. 501–517, 2004](https://people.maths.ox.ac.uk/trefethen/barycentric.pdf).
321    ///
322    /// # Warning
323    ///
324    /// This function assumes that each evaluation has a unique index. If there are duplicate indices, the function may
325    /// fail with an error when attempting to compute the inverse of zero.
326    pub fn recover<'a, I>(t: u32, evals: I) -> Result<C, Error>
327    where
328        C: 'a,
329        I: IntoIterator<Item = &'a Eval<C>>,
330    {
331        // Prepare evaluations
332        let evals = prepare_evaluations(t, evals)?;
333
334        // Compute weights
335        let indices = evals.iter().map(|e| e.index).collect::<Vec<_>>();
336        let weights = compute_weights(indices)?;
337
338        // Perform interpolation using the precomputed weights
339        Self::recover_with_weights(&weights, evals)
340    }
341}
342
343impl<C: Element> Write for Poly<C> {
344    fn write(&self, buf: &mut impl BufMut) {
345        for c in &self.0 {
346            c.write(buf);
347        }
348    }
349}
350
351impl<C: Element> Read for Poly<C> {
352    type Cfg = usize;
353
354    fn read_cfg(buf: &mut impl Buf, expected: &Self::Cfg) -> Result<Self, CodecError> {
355        let expected_size = C::SIZE * (*expected);
356        if buf.remaining() < expected_size {
357            return Err(CodecError::EndOfBuffer);
358        }
359        let mut coeffs = Vec::with_capacity(*expected);
360        for _ in 0..*expected {
361            coeffs.push(C::read(buf)?);
362        }
363        Ok(Self(coeffs))
364    }
365}
366
367impl<C: Element> EncodeSize for Poly<C> {
368    fn encode_size(&self) -> usize {
369        C::SIZE * self.0.len()
370    }
371}
372
373/// Returns the public key of the polynomial (constant term).
374pub fn public<V: Variant>(public: &Public<V>) -> &V::Public {
375    public.constant()
376}
377
378#[cfg(test)]
379pub mod tests {
380    // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/b0ef82ff79769d085a5a7d3f4fe690b1c8fe6dc9/crates/threshold-bls/src/poly.rs#L355-L604
381    use super::*;
382    use crate::bls12381::primitives::group::{Scalar, G2};
383    use commonware_codec::{Decode, Encode};
384
385    #[test]
386    fn poly_degree() {
387        let s = 5;
388        let p = new(s);
389        assert_eq!(p.degree(), s);
390    }
391
392    #[test]
393    fn add_zero() {
394        let p1 = new(3);
395        let p2 = Poly::<Scalar>::zero();
396        let mut res = p1.clone();
397        res.add(&p2);
398        assert_eq!(res, p1);
399
400        let p1 = Poly::<Scalar>::zero();
401        let p2 = new(3);
402        let mut res = p1;
403        res.add(&p2);
404        assert_eq!(res, p2);
405    }
406
407    #[test]
408    fn interpolation_insufficient_shares() {
409        let degree = 4;
410        let threshold = degree + 1;
411        let poly = new(degree);
412        let shares = (0..threshold - 1)
413            .map(|i| poly.evaluate(i))
414            .collect::<Vec<_>>();
415        Poly::recover(threshold, &shares).unwrap_err();
416    }
417
418    #[test]
419    fn evaluate_with_overflow() {
420        let degree = 4;
421        let poly = new(degree);
422        poly.evaluate(u32::MAX);
423    }
424
425    #[test]
426    fn commit() {
427        let secret = new(5);
428        let coeffs = secret.0.clone();
429        let commitment = coeffs
430            .iter()
431            .map(|coeff| {
432                let mut p = G2::one();
433                p.mul(coeff);
434                p
435            })
436            .collect::<Vec<_>>();
437        let commitment = Poly::from(commitment);
438        assert_eq!(commitment, Poly::commit(secret));
439    }
440
441    fn pow(base: Scalar, pow: usize) -> Scalar {
442        let mut res = Scalar::one();
443        for _ in 0..pow {
444            res.mul(&base)
445        }
446        res
447    }
448
449    #[test]
450    fn addition() {
451        for deg1 in 0..100u32 {
452            for deg2 in 0..100u32 {
453                let p1 = new(deg1);
454                let p2 = new(deg2);
455                let mut res = p1.clone();
456                res.add(&p2);
457
458                let (larger, smaller) = if p1.degree() > p2.degree() {
459                    (&p1, &p2)
460                } else {
461                    (&p2, &p1)
462                };
463
464                for i in 0..larger.degree() + 1 {
465                    let i = i as usize;
466                    if i < (smaller.degree() + 1) as usize {
467                        let mut coeff_sum = p1.0[i].clone();
468                        coeff_sum.add(&p2.0[i]);
469                        assert_eq!(res.0[i], coeff_sum);
470                    } else {
471                        assert_eq!(res.0[i], larger.0[i]);
472                    }
473                }
474                assert_eq!(res.degree(), larger.degree(), "deg1={deg1}, deg2={deg2}");
475            }
476        }
477    }
478
479    #[test]
480    fn interpolation() {
481        for degree in 0..100u32 {
482            for num_evals in 0..100u32 {
483                let poly = new(degree);
484                let expected = poly.0[0].clone();
485
486                let shares = (0..num_evals).map(|i| poly.evaluate(i)).collect::<Vec<_>>();
487                let recovered_constant = Poly::recover(num_evals, &shares).unwrap();
488
489                if num_evals > degree {
490                    assert_eq!(
491                        expected, recovered_constant,
492                        "degree={degree}, num_evals={num_evals}"
493                    );
494                } else {
495                    assert_ne!(
496                        expected, recovered_constant,
497                        "degree={degree}, num_evals={num_evals}"
498                    );
499                }
500            }
501        }
502    }
503
504    #[test]
505    fn evaluate() {
506        for d in 0..100u32 {
507            for idx in 0..100_u32 {
508                let x = Scalar::from_index(idx);
509
510                let p1 = new(d);
511                let evaluation = p1.evaluate(idx).value;
512
513                let coeffs = p1.0;
514                let mut sum = coeffs[0].clone();
515                for (i, coeff) in coeffs
516                    .into_iter()
517                    .enumerate()
518                    .take((d + 1) as usize)
519                    .skip(1)
520                {
521                    let xi = pow(x.clone(), i);
522                    let mut var = coeff;
523                    var.mul(&xi);
524                    sum.add(&var);
525                }
526
527                assert_eq!(sum, evaluation, "degree={d}, idx={idx}");
528            }
529        }
530    }
531
532    #[test]
533    fn test_codec() {
534        let original = new(5);
535        let encoded = original.encode();
536        let decoded = Poly::<Scalar>::decode_cfg(encoded, &(original.required() as usize)).unwrap();
537        assert_eq!(original, decoded);
538    }
539}