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 xi = Scalar::from_index(*i);
136
137 // Compute product terms for Lagrange basis polynomial
138 let (mut num, mut den) = (Scalar::one(), Scalar::one());
139 for j in &indices {
140 // Skip if i_eval and j_eval are the same
141 if i == j {
142 continue;
143 }
144
145 // Convert j_eval.index to x-coordinate
146 let xj = Scalar::from_index(*j);
147
148 // Include `xj` in the numerator product for `l_i(0)`
149 num.mul(&xj);
150
151 // Compute `xj - xi` and include it in the denominator product
152 let mut diff = xj;
153 diff.sub(&xi);
154 den.mul(&diff);
155 }
156
157 // Compute the inverse of the denominator product; fails if den is zero (e.g., duplicate `xj`)
158 let inv = den.inverse().ok_or(Error::NoInverse)?;
159
160 // Compute `l_i(0) = num * inv`, the Lagrange basis coefficient at `x=0`
161 num.mul(&inv);
162
163 // Store the weight
164 weights.insert(*i, Weight(num));
165 }
166 Ok(weights)
167}
168
169impl<C> Poly<C> {
170 /// Creates a new polynomial from the given coefficients.
171 pub fn from(c: Vec<C>) -> Self {
172 Self(c)
173 }
174
175 /// Returns the constant term of the polynomial.
176 pub fn constant(&self) -> &C {
177 &self.0[0]
178 }
179
180 /// Returns the degree of the polynomial
181 pub fn degree(&self) -> u32 {
182 (self.0.len() - 1) as u32 // check size in deserialize, safe to cast
183 }
184
185 /// Returns the number of required shares to reconstruct the polynomial.
186 ///
187 /// This will be the threshold
188 pub fn required(&self) -> u32 {
189 self.0.len() as u32 // check size in deserialize, safe to cast
190 }
191}
192
193impl<C: Element> Poly<C> {
194 /// Commits the scalar polynomial to the group and returns a polynomial over
195 /// the group.
196 ///
197 /// This is done by multiplying each coefficient of the polynomial with the
198 /// group's generator.
199 pub fn commit(commits: Poly<Scalar>) -> Self {
200 // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L322-L340
201 let commits = commits
202 .0
203 .iter()
204 .map(|c| {
205 let mut commitment = C::one();
206 commitment.mul(c);
207 commitment
208 })
209 .collect::<Vec<C>>();
210
211 Poly::<C>::from(commits)
212 }
213
214 /// Returns a zero polynomial.
215 pub fn zero() -> Self {
216 Self::from(vec![C::zero()])
217 }
218
219 /// Returns the given coefficient at the requested index.
220 ///
221 /// It panics if the index is out of range.
222 pub fn get(&self, i: u32) -> C {
223 self.0[i as usize].clone()
224 }
225
226 /// Set the given element at the specified index.
227 ///
228 /// It panics if the index is out of range.
229 pub fn set(&mut self, index: u32, value: C) {
230 self.0[index as usize] = value;
231 }
232
233 /// Performs polynomial addition in place.
234 pub fn add(&mut self, other: &Self) {
235 // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L87-L95
236
237 // if we have a smaller degree we should pad with zeros
238 if self.0.len() < other.0.len() {
239 self.0.resize(other.0.len(), C::zero())
240 }
241
242 self.0.iter_mut().zip(&other.0).for_each(|(a, b)| a.add(b))
243 }
244
245 /// Evaluates the polynomial at the specified index (provided value offset by 1).
246 pub fn evaluate(&self, index: u32) -> Eval<C> {
247 // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/a714310be76620e10e8797d6637df64011926430/crates/threshold-bls/src/poly.rs#L111-L129
248
249 // We add +1 because we must never evaluate the polynomial at its first point
250 // otherwise it reveals the "secret" value after a reshare (where the constant
251 // term is set to be the secret of the previous dealing).
252 let xi = Scalar::from_index(index);
253
254 // Use Horner's method to evaluate the polynomial
255 let value = self.0.iter().rev().fold(C::zero(), |mut sum, coeff| {
256 sum.mul(&xi);
257 sum.add(coeff);
258 sum
259 });
260 Eval { value, index }
261 }
262
263 /// Recovers the constant term of a polynomial of degree less than `t` using `t` evaluations of the polynomial
264 /// and precomputed Barycentric Weights.
265 ///
266 /// This function uses Lagrange interpolation to compute the constant term (i.e., the value of the polynomial at `x=0`)
267 /// given at least `t` distinct evaluations of the polynomial. Each evaluation is assumed to have a unique index,
268 /// which is mapped to a unique x-value as `x = index + 1`.
269 ///
270 /// # References
271 ///
272 /// This implementation is based on [J.-P. Berrut and L. N.
273 /// Trefethen, “Barycentric Lagrange Interpolation,” SIAM Rev., vol. 46, no. 3,
274 /// pp. 501–517, 2004](https://people.maths.ox.ac.uk/trefethen/barycentric.pdf).
275 ///
276 /// # Warning
277 ///
278 /// This function assumes that each evaluation has a unique index. If there are duplicate indices, the function may
279 /// fail with an error when attempting to compute the inverse of zero.
280 pub fn recover_with_weights<'a, I>(
281 weights: &BTreeMap<u32, Weight>,
282 evals: I,
283 ) -> Result<C, Error>
284 where
285 C: 'a,
286 I: IntoIterator<Item = &'a Eval<C>>,
287 {
288 // Scale all evaluations by their corresponding weight
289 let mut result = C::zero();
290 for eval in evals.into_iter() {
291 // Get the weight for the current evaluation index
292 let Some(weight) = weights.get(&eval.index) else {
293 return Err(Error::InvalidIndex);
294 };
295
296 // Scale `yi` by `l_i(0)` to contribute to the constant term
297 let mut scaled_value = eval.value.clone();
298 scaled_value.mul(&weight.0);
299
300 // Add `yi * l_i(0)` to the running sum
301 result.add(&scaled_value);
302 }
303
304 Ok(result)
305 }
306
307 /// Recovers the constant term of a polynomial of degree less than `t` using at least `t` evaluations of
308 /// the polynomial.
309 ///
310 /// This function uses Lagrange interpolation to compute the constant term (i.e., the value of the polynomial at `x=0`)
311 /// given at least `t` distinct evaluations of the polynomial. Each evaluation is assumed to have a unique index,
312 /// which is mapped to a unique x-value as `x = index + 1`.
313 ///
314 /// # References
315 ///
316 /// This implementation is based on [J.-P. Berrut and L. N.
317 /// Trefethen, “Barycentric Lagrange Interpolation,” SIAM Rev., vol. 46, no. 3,
318 /// pp. 501–517, 2004](https://people.maths.ox.ac.uk/trefethen/barycentric.pdf).
319 ///
320 /// # Warning
321 ///
322 /// This function assumes that each evaluation has a unique index. If there are duplicate indices, the function may
323 /// fail with an error when attempting to compute the inverse of zero.
324 pub fn recover<'a, I>(t: u32, evals: I) -> Result<C, Error>
325 where
326 C: 'a,
327 I: IntoIterator<Item = &'a Eval<C>>,
328 {
329 // Prepare evaluations
330 let evals = prepare_evaluations(t, evals)?;
331
332 // Compute weights
333 let indices = evals.iter().map(|e| e.index).collect::<Vec<_>>();
334 let weights = compute_weights(indices)?;
335
336 // Perform interpolation using the precomputed weights
337 Self::recover_with_weights(&weights, evals)
338 }
339}
340
341impl<C: Element> Write for Poly<C> {
342 fn write(&self, buf: &mut impl BufMut) {
343 for c in &self.0 {
344 c.write(buf);
345 }
346 }
347}
348
349impl<C: Element> Read for Poly<C> {
350 type Cfg = usize;
351
352 fn read_cfg(buf: &mut impl Buf, expected: &Self::Cfg) -> Result<Self, CodecError> {
353 let expected_size = C::SIZE * (*expected);
354 if buf.remaining() < expected_size {
355 return Err(CodecError::EndOfBuffer);
356 }
357 let mut coeffs = Vec::with_capacity(*expected);
358 for _ in 0..*expected {
359 coeffs.push(C::read(buf)?);
360 }
361 Ok(Self(coeffs))
362 }
363}
364
365impl<C: Element> EncodeSize for Poly<C> {
366 fn encode_size(&self) -> usize {
367 C::SIZE * self.0.len()
368 }
369}
370
371/// Returns the public key of the polynomial (constant term).
372pub fn public<V: Variant>(public: &Public<V>) -> &V::Public {
373 public.constant()
374}
375
376#[cfg(test)]
377pub mod tests {
378 // Reference: https://github.com/celo-org/celo-threshold-bls-rs/blob/b0ef82ff79769d085a5a7d3f4fe690b1c8fe6dc9/crates/threshold-bls/src/poly.rs#L355-L604
379 use super::*;
380 use crate::bls12381::primitives::group::{Scalar, G2};
381 use commonware_codec::{Decode, Encode};
382
383 #[test]
384 fn poly_degree() {
385 let s = 5;
386 let p = new(s);
387 assert_eq!(p.degree(), s);
388 }
389
390 #[test]
391 fn add_zero() {
392 let p1 = new(3);
393 let p2 = Poly::<Scalar>::zero();
394 let mut res = p1.clone();
395 res.add(&p2);
396 assert_eq!(res, p1);
397
398 let p1 = Poly::<Scalar>::zero();
399 let p2 = new(3);
400 let mut res = p1;
401 res.add(&p2);
402 assert_eq!(res, p2);
403 }
404
405 #[test]
406 fn interpolation_insufficient_shares() {
407 let degree = 4;
408 let threshold = degree + 1;
409 let poly = new(degree);
410 let shares = (0..threshold - 1)
411 .map(|i| poly.evaluate(i))
412 .collect::<Vec<_>>();
413 Poly::recover(threshold, &shares).unwrap_err();
414 }
415
416 #[test]
417 fn evaluate_with_overflow() {
418 let degree = 4;
419 let poly = new(degree);
420 poly.evaluate(u32::MAX);
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!(res.degree(), larger.degree(), "deg1={deg1}, deg2={deg2}");
473 }
474 }
475 }
476
477 #[test]
478 fn interpolation() {
479 for degree in 0..100u32 {
480 for num_evals in 0..100u32 {
481 let poly = new(degree);
482 let expected = poly.0[0].clone();
483
484 let shares = (0..num_evals).map(|i| poly.evaluate(i)).collect::<Vec<_>>();
485 let recovered_constant = Poly::recover(num_evals, &shares).unwrap();
486
487 if num_evals > degree {
488 assert_eq!(
489 expected, recovered_constant,
490 "degree={degree}, num_evals={num_evals}"
491 );
492 } else {
493 assert_ne!(
494 expected, recovered_constant,
495 "degree={degree}, num_evals={num_evals}"
496 );
497 }
498 }
499 }
500 }
501
502 #[test]
503 fn evaluate() {
504 for d in 0..100u32 {
505 for idx in 0..100_u32 {
506 let x = Scalar::from_index(idx);
507
508 let p1 = new(d);
509 let evaluation = p1.evaluate(idx).value;
510
511 let coeffs = p1.0;
512 let mut sum = coeffs[0].clone();
513 for (i, coeff) in coeffs
514 .into_iter()
515 .enumerate()
516 .take((d + 1) as usize)
517 .skip(1)
518 {
519 let xi = pow(x.clone(), i);
520 let mut var = coeff;
521 var.mul(&xi);
522 sum.add(&var);
523 }
524
525 assert_eq!(sum, evaluation, "degree={d}, idx={idx}");
526 }
527 }
528 }
529
530 #[test]
531 fn test_codec() {
532 let original = new(5);
533 let encoded = original.encode();
534 let decoded = Poly::<Scalar>::decode_cfg(encoded, &(original.required() as usize)).unwrap();
535 assert_eq!(original, decoded);
536 }
537}