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}