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}