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}