From 15f0f9e65c3dff95da7821b3614ee1563433553b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Tue, 30 Sep 2025 15:35:58 +0200 Subject: [PATCH 01/21] Add polynomial xgcd --- fastcrypto-tbls/src/polynomial.rs | 149 +++++++++++++++++- fastcrypto-tbls/src/tests/polynomial_tests.rs | 25 +++ 2 files changed, 169 insertions(+), 5 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index d74798306..af589792d 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -13,7 +13,8 @@ use itertools::{Either, Itertools}; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::HashSet; -use std::ops::{AddAssign, Mul}; +use std::mem::swap; +use std::ops::{AddAssign, Div, Mul, SubAssign}; /// Types @@ -29,18 +30,26 @@ pub type PublicPoly = Poly; /// Vector related operations. -impl Poly { +impl Poly { /// Returns the degree of the polynomial pub fn degree(&self) -> usize { // e.g. c_0 + c_1 * x + c_2 * x^2 + c_3 * x^3 // ^ 4 coefficients correspond to a 3rd degree poly self.0.len() - 1 } + + fn reduce(&mut self) { + while self.0.len() > 1 && self.0.last() == Some(&C::zero()) { + self.0.pop(); + } + } } -impl From> for Poly { +impl From> for Poly { fn from(c: Vec) -> Self { - Self(c) + let mut p = Self(c); + p.reduce(); + p } } @@ -51,6 +60,7 @@ impl AddAssign<&Self> for Poly { if self.0.len() < other.0.len() { self.0.extend_from_slice(&other.0[self.0.len()..]); } + self.reduce(); } } @@ -58,7 +68,24 @@ impl Mul<&C> for Poly { type Output = Poly; fn mul(self, rhs: &C) -> Self::Output { - Poly(self.0.into_iter().map(|c| c * rhs).collect()) + Poly::from(self.0.into_iter().map(|c| c * rhs).collect_vec()) + } +} + +impl Mul<&Poly> for &Poly { + type Output = Poly; + + fn mul(self, rhs: &Poly) -> Poly { + if self.is_zero() || rhs.is_zero() { + return Poly::zero(); + } + let mut result = vec![C::zero(); self.degree() + rhs.degree() + 1]; + for (i, a) in self.0.iter().enumerate() { + for (j, b) in rhs.0.iter().enumerate() { + result[i + j] += *a * *b; + } + } + Poly::from(result) } } @@ -288,3 +315,115 @@ impl Poly { Ok(res) } } + +struct Monomial { + coefficient: C, + degree: usize, +} + +impl Div<&Self> for Monomial { + type Output = Monomial; + + fn div(self, rhs: &Self) -> Self { + if rhs.coefficient == C::zero() { + panic!("Division by zero monomial"); + } + if self.degree < rhs.degree { + panic!("Division would result in negative degree"); + } + Monomial { + coefficient: (self.coefficient / rhs.coefficient) + .expect("Safe since rhs.coefficient != 0"), + degree: self.degree - rhs.degree, + } + } +} + +impl AddAssign<&Monomial> for Poly { + fn add_assign(&mut self, rhs: &Monomial) { + if self.0.len() <= rhs.degree { + self.0.resize(rhs.degree + 1, C::zero()); + } + self.0[rhs.degree] += rhs.coefficient; + self.reduce(); + } +} + +impl SubAssign> for Poly { + fn sub_assign(&mut self, rhs: Poly) { + if self.0.len() < rhs.0.len() { + self.0.resize(rhs.0.len(), C::zero()); + } + for (a, b) in self.0.iter_mut().zip(&rhs.0) { + *a -= *b; + } + self.reduce(); + } +} + +impl Mul<&Monomial> for &Poly { + type Output = Poly; + + fn mul(self, rhs: &Monomial) -> Poly { + if rhs.coefficient == C::zero() { + return Poly::zero(); + } + let mut result = vec![C::zero(); self.degree() + rhs.degree + 1]; + for (i, coefficient) in self.0.iter().enumerate() { + result[i + rhs.degree] = *coefficient * rhs.coefficient; + } + Poly::from(result) + } +} + +impl Poly { + fn is_zero(&self) -> bool { + self.0.len() == 1 && self.0[0] == C::zero() + } + + fn one() -> Self { + Self::from(vec![C::generator()]) + } + + fn lead(&self) -> Monomial { + if self.is_zero() { + return Monomial { + coefficient: C::zero(), + degree: 0, + }; + } + Monomial { + coefficient: *self.0.last().unwrap(), + degree: self.degree(), + } + } + + pub fn div_rem(&self, divisor: &Poly) -> FastCryptoResult<(Poly, Poly)> { + if divisor.is_zero() { + return Err(FastCryptoError::InvalidInput); + } + let mut remainder = self.clone(); + let mut quotient = Self::zero(); + while !remainder.is_zero() && remainder.degree() >= divisor.degree() { + let tmp = remainder.lead() / &divisor.lead(); + quotient += &tmp; + remainder -= divisor * &tmp; + } + Ok((quotient, remainder)) + } + + pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { + let mut r = (self.clone(), other.clone()); + let mut s = (Poly::one(), Poly::zero()); + let mut t = (Poly::zero(), Poly::one()); + while !r.1.is_zero() { + let (q, r_new) = r.0.div_rem(&r.1)?; + r = (r.1, r_new); + s.0 -= &q * &s.1; + swap(&mut s.0, &mut s.1); + t.0 -= &q * &t.1; + swap(&mut t.0, &mut t.1); + } + Ok((r.0, s.0, t.0)) + } +} diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index 9a35c3bdc..d69073960 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -10,7 +10,9 @@ use crate::types::ShareIndex; use fastcrypto::groups::bls12381::{G1Element, G2Element, Scalar as BlsScalar}; use fastcrypto::groups::ristretto255::{RistrettoPoint, RistrettoScalar}; use fastcrypto::groups::{GroupElement, MultiScalarMul, Scalar}; +use itertools::Itertools; use rand::prelude::*; +use std::iter; use std::num::NonZeroU16; const I10: NonZeroU16 = unsafe { NonZeroU16::new_unchecked(10) }; @@ -237,3 +239,26 @@ mod points_tests { #[instantiate_tests()] mod g2_element {} } + +#[test] +fn test_xgcd() { + let mut rng = thread_rng(); + let degree_a = 7; + let degree_b = 5; + let a = Poly::from( + iter::from_fn(|| Some(BlsScalar::rand(&mut rng))) + .take(degree_a + 1) + .collect_vec(), + ); + let b = Poly::from( + iter::from_fn(|| Some(BlsScalar::rand(&mut rng))) + .take(degree_b + 1) + .collect_vec(), + ); + + let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); + + let mut lhs = &x * &a; + lhs += &(&y * &b); + assert_eq!(lhs, g); +} From 54812a1f85817a4e1276cd7864a55a72f58a0fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Tue, 30 Sep 2025 16:48:29 +0200 Subject: [PATCH 02/21] Test division --- fastcrypto-tbls/src/polynomial.rs | 4 +- fastcrypto-tbls/src/tests/polynomial_tests.rs | 71 ++++++++++++------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index af589792d..1efc99ca1 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -419,9 +419,11 @@ impl Poly { while !r.1.is_zero() { let (q, r_new) = r.0.div_rem(&r.1)?; r = (r.1, r_new); + s.0 -= &q * &s.1; - swap(&mut s.0, &mut s.1); t.0 -= &q * &t.1; + + swap(&mut s.0, &mut s.1); swap(&mut t.0, &mut t.1); } Ok((r.0, s.0, t.0)) diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index d69073960..539257a5f 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -10,7 +10,6 @@ use crate::types::ShareIndex; use fastcrypto::groups::bls12381::{G1Element, G2Element, Scalar as BlsScalar}; use fastcrypto::groups::ristretto255::{RistrettoPoint, RistrettoScalar}; use fastcrypto::groups::{GroupElement, MultiScalarMul, Scalar}; -use itertools::Itertools; use rand::prelude::*; use std::iter; use std::num::NonZeroU16; @@ -114,6 +113,53 @@ mod scalar_tests { Poly::interpolate_at_index(ShareIndex::new(7).unwrap(), &shares).unwrap_err(); } + #[test] + fn test_division() { + let mut rng = thread_rng(); + let degree_a = 8; + let degree_b = 5; + let a = crate::polynomial::Poly::from( + iter::from_fn(|| Some(S::rand(&mut rng))) + .take(degree_a + 1) + .collect_vec(), + ); + let b = crate::polynomial::Poly::from( + iter::from_fn(|| Some(S::rand(&mut rng))) + .take(degree_b + 1) + .collect_vec(), + ); + + let (q, r) = a.div_rem(&b).unwrap(); + assert!(r.degree() < b.degree()); + + let mut lhs = &q * &b; + lhs += &r; + assert_eq!(lhs, a); + } + + #[test] + fn test_extended_gcd() { + let mut rng = thread_rng(); + let degree_a = 8; + let degree_b = 5; + let a = crate::polynomial::Poly::from( + iter::from_fn(|| Some(S::rand(&mut rng))) + .take(degree_a + 1) + .collect_vec(), + ); + let b = crate::polynomial::Poly::from( + iter::from_fn(|| Some(S::rand(&mut rng))) + .take(degree_b + 1) + .collect_vec(), + ); + + let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); + + let mut lhs = &x * &a; + lhs += &(&y * &b); + assert_eq!(lhs, g); + } + #[instantiate_tests()] mod ristretto_scalar {} @@ -239,26 +285,3 @@ mod points_tests { #[instantiate_tests()] mod g2_element {} } - -#[test] -fn test_xgcd() { - let mut rng = thread_rng(); - let degree_a = 7; - let degree_b = 5; - let a = Poly::from( - iter::from_fn(|| Some(BlsScalar::rand(&mut rng))) - .take(degree_a + 1) - .collect_vec(), - ); - let b = Poly::from( - iter::from_fn(|| Some(BlsScalar::rand(&mut rng))) - .take(degree_b + 1) - .collect_vec(), - ); - - let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); - - let mut lhs = &x * &a; - lhs += &(&y * &b); - assert_eq!(lhs, g); -} From b57544edf565d3e61415b2a91bc473bb0d8a2828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Tue, 30 Sep 2025 16:54:49 +0200 Subject: [PATCH 03/21] Canonical repr --- fastcrypto-tbls/src/polynomial.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 1efc99ca1..eb4497da6 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -47,6 +47,9 @@ impl Poly { impl From> for Poly { fn from(c: Vec) -> Self { + if c.is_empty() { + return Self::zero(); + } let mut p = Self(c); p.reduce(); p @@ -398,6 +401,8 @@ impl Poly { } } + /// Divide self by divisor, returning the quotient and remainder. + /// Returns an error if divisor is zero. pub fn div_rem(&self, divisor: &Poly) -> FastCryptoResult<(Poly, Poly)> { if divisor.is_zero() { return Err(FastCryptoError::InvalidInput); @@ -412,6 +417,8 @@ impl Poly { Ok((quotient, remainder)) } + /// Compute the extended GCD of two polynomials. + /// Returns (g, x, y) such that g = self * x + other * y. pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { let mut r = (self.clone(), other.clone()); let mut s = (Poly::one(), Poly::zero()); From 2989cf575d5f598587f06527f525f4f8e9edee8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Wed, 1 Oct 2025 15:06:14 +0200 Subject: [PATCH 04/21] simplify --- fastcrypto-tbls/src/polynomial.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index eb4497da6..87f04b9a8 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -325,20 +325,16 @@ struct Monomial { } impl Div<&Self> for Monomial { - type Output = Monomial; + type Output = FastCryptoResult>; - fn div(self, rhs: &Self) -> Self { - if rhs.coefficient == C::zero() { - panic!("Division by zero monomial"); - } + fn div(self, rhs: &Self) -> Self::Output { if self.degree < rhs.degree { - panic!("Division would result in negative degree"); + return Err(FastCryptoError::InvalidInput); } - Monomial { - coefficient: (self.coefficient / rhs.coefficient) - .expect("Safe since rhs.coefficient != 0"), + Ok(Monomial { + coefficient: (self.coefficient / rhs.coefficient)?, degree: self.degree - rhs.degree, - } + }) } } @@ -410,7 +406,8 @@ impl Poly { let mut remainder = self.clone(); let mut quotient = Self::zero(); while !remainder.is_zero() && remainder.degree() >= divisor.degree() { - let tmp = remainder.lead() / &divisor.lead(); + // TODO: The divisor lead is always the same, so we can precompute its inverse. + let tmp = (remainder.lead() / &divisor.lead())?; quotient += &tmp; remainder -= divisor * &tmp; } From 3ba7cab24caa7f4a78d93875ec976a5afaf84678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 2 Oct 2025 11:53:38 +0200 Subject: [PATCH 05/21] Add rs decoder --- fastcrypto-tbls/src/polynomial.rs | 65 +++++++-- fastcrypto-tbls/src/tests/polynomial_tests.rs | 43 +++++- fastcrypto-tbls/src/threshold_schnorr/gao.rs | 136 ++++++++++++++++++ fastcrypto-tbls/src/threshold_schnorr/mod.rs | 1 + fastcrypto/src/error.rs | 4 + 5 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 fastcrypto-tbls/src/threshold_schnorr/gao.rs diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 87f04b9a8..606674b18 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::HashSet; use std::mem::swap; -use std::ops::{AddAssign, Div, Mul, SubAssign}; +use std::ops::{Add, AddAssign, Div, Mul, SubAssign}; /// Types @@ -92,6 +92,15 @@ impl Mul<&Poly> for &Poly { } } +impl Add<&Poly> for Poly { + type Output = Poly; + + fn add(mut self, rhs: &Poly) -> Poly { + self += rhs; + self + } +} + /// GroupElement operations. impl Poly { @@ -100,6 +109,10 @@ impl Poly { Self::from(vec![C::zero()]) } + pub fn one() -> Self { + Self::from(vec![C::generator()]) + } + // TODO: Some of the functions/steps below may be executed many times in practice thus cache can be // used to improve efficiency (e.g., eval(i) may be called with the same index every time a partial // signature from party i is verified). @@ -303,6 +316,33 @@ impl Poly { Ok(Eval { index, value }) } + + /// Given a set of shares with unique indices, compute the polynomial that + /// goes through all the points. The degree of the resulting polynomial is + /// at most `points.len() - 1`. + /// Returns an error if the input is invalid (e.g., empty or duplicate indices). + pub fn interpolate(points: &[Eval]) -> FastCryptoResult> { + if points.is_empty() || !points.iter().map(|p| p.index).all_unique() { + return Err(FastCryptoError::InvalidInput); + } + let result = points + .iter() + .map(|p_j| { + let x_j = C::from(p_j.index.get() as u128); + points + .iter() + .filter(|p_i| p_i.index != p_j.index) + .map(|p_i| { + let x_i = C::from(p_i.index.get() as u128); + Poly::from(vec![-x_i, C::generator()]) + * &(x_j - x_i).inverse().expect("Divisor is never zero") + }) + .fold(Poly::::one(), |product, factor| &product * &factor) + * &p_j.value + }) + .fold(Poly::zero(), |acc, p| acc + &p); + Ok(result) + } } impl Poly { @@ -376,12 +416,8 @@ impl Mul<&Monomial> for &Poly { } impl Poly { - fn is_zero(&self) -> bool { - self.0.len() == 1 && self.0[0] == C::zero() - } - - fn one() -> Self { - Self::from(vec![C::generator()]) + pub(crate) fn is_zero(&self) -> bool { + self.0 == vec![C::zero()] } fn lead(&self) -> Monomial { @@ -415,12 +451,17 @@ impl Poly { } /// Compute the extended GCD of two polynomials. - /// Returns (g, x, y) such that g = self * x + other * y. - pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { + /// Returns (g, x, y, s, t) such that g = self * x + other * y. + /// + pub fn partial_extended_gcd( + &self, + other: &Poly, + degree_bound: usize, + ) -> FastCryptoResult<(Poly, Poly, Poly)> { let mut r = (self.clone(), other.clone()); let mut s = (Poly::one(), Poly::zero()); let mut t = (Poly::zero(), Poly::one()); - while !r.1.is_zero() { + while r.0.degree() >= degree_bound && !r.1.is_zero() { let (q, r_new) = r.0.div_rem(&r.1)?; r = (r.1, r_new); @@ -432,4 +473,8 @@ impl Poly { } Ok((r.0, s.0, t.0)) } + + pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { + self.partial_extended_gcd(other, 0) + } } diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index 539257a5f..f1f71a4db 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -113,6 +113,45 @@ mod scalar_tests { Poly::interpolate_at_index(ShareIndex::new(7).unwrap(), &shares).unwrap_err(); } + #[test] + fn test_interpolate() { + let degree = 12; + let threshold = degree + 1; + let poly = Poly::::rand(degree, &mut thread_rng()); + let mut shares = (1..50) + .map(|i| poly.eval(ShareIndex::new(i).unwrap())) + .collect::>(); + for _ in 0..10 { + shares.shuffle(&mut thread_rng()); + let used_shares = shares + .iter() + .take(threshold as usize) + .cloned() + .collect_vec(); + let interpolated = Poly::interpolate(&used_shares).unwrap(); + assert_eq!(interpolated, poly); + } + + // Using too few shares + for _ in 0..10 { + shares.shuffle(&mut thread_rng()); + let used_shares = shares + .iter() + .take(threshold as usize - 1) + .cloned() + .collect_vec(); + let interpolated = Poly::interpolate(&used_shares).unwrap(); + assert_ne!(interpolated, poly); + } + + // Using duplicate shares should fail + let mut shares = (1..=threshold) + .map(|i| poly.eval(ShareIndex::new(i).unwrap())) + .collect_vec(); // duplicate value 1 + shares.push(poly.eval(ShareIndex::new(1).unwrap())); + Poly::interpolate(&shares).unwrap_err(); + } + #[test] fn test_division() { let mut rng = thread_rng(); @@ -155,9 +194,7 @@ mod scalar_tests { let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); - let mut lhs = &x * &a; - lhs += &(&y * &b); - assert_eq!(lhs, g); + assert_eq!(&x * &a + &(&y * &b), g); } #[instantiate_tests()] diff --git a/fastcrypto-tbls/src/threshold_schnorr/gao.rs b/fastcrypto-tbls/src/threshold_schnorr/gao.rs new file mode 100644 index 000000000..4a665a882 --- /dev/null +++ b/fastcrypto-tbls/src/threshold_schnorr/gao.rs @@ -0,0 +1,136 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::polynomial::{Eval, Poly}; +use crate::threshold_schnorr::S; +use crate::types::ShareIndex; +use fastcrypto::error::{FastCryptoError, FastCryptoResult}; +use fastcrypto::groups::GroupElement; +use itertools::Itertools; + +/// Decoder for Reed-Solomon codes. +/// This can correct up to (d-1)/2 errors, where d is the distance of the code. +/// The code is defined by the evaluation points `a` and the message length `k`. +/// The distance is given by `n - k + 1`, where `n` is the length of `a`. +/// +/// The implementation follows the Gao decoding algorithm (see https://www.math.clemson.edu/~sgao/papers/RS.pdf). +pub struct RSDecoder { + g0: Poly, + a: Vec, + k: usize, +} + +impl RSDecoder { + /// Create a new Gao decoder with the given evaluation points `a` and message length `k`. + pub fn new(a: Vec, k: usize) -> Self { + assert!(k < a.len(), "Message length must be less than block length"); + let g0 = a + .iter() + .map(|ai| S::from(ai.get() as u128)) + .fold(Poly::one(), |acc, ai| { + &acc * &Poly::from(vec![-ai, S::generator()]) + }); + Self { g0, a, k } + } + + /// The length of the code words. + fn block_length(&self) -> usize { + self.a.len() + } + + /// The length of the messages. + fn message_length(&self) -> usize { + self.k + } + + /// The distance of the code. + fn distance(&self) -> usize { + self.block_length() - self.message_length() + 1 + } + + /// Compute the message polynomial. + /// Returns an error if the input length is wrong or if there are too many errors to correct. + pub fn compute_message_polynomial(&self, code_word: &[S]) -> FastCryptoResult> { + // The implementation follows Algorithm 1 in Gao's paper. + + if code_word.len() != self.block_length() { + return Err(FastCryptoError::InputLengthWrong(self.block_length())); + } + + // Step 1: Interpolation + let g1 = Poly::interpolate( + &self + .a + .iter() + .zip(code_word) + .map(|(index, value)| Eval { + index: *index, + value: *value, + }) + .collect_vec(), + )?; + + // Step 2: Partial GCD + let (g, _, v) = Poly::partial_extended_gcd( + &self.g0, + &g1, + (self.message_length() + self.block_length()) / 2, + )?; + + // Step 3: Long division + let (f1, r) = g.div_rem(&v)?; + if !r.is_zero() || f1.degree() >= self.k { + return Err(FastCryptoError::TooManyErrors((self.distance() - 1) / 2)); + } + Ok(f1) + } + + /// Encode the message using the Reed-Solomon code defined by the evaluation points `a`. + /// Returns an error if the message length is wrong. + #[cfg(test)] + fn encode(&self, message: Vec) -> FastCryptoResult> { + if message.len() != self.message_length() { + return Err(FastCryptoError::InputLengthWrong(self.message_length())); + } + let f = Poly::from(message); + Ok(self.a.iter().map(|ai| f.eval(*ai).value).collect_vec()) + } + + /// Try to correct the input and return the decoded message. + /// Returns an error if the input length is wrong or if there are too many errors to correct. + pub fn decode(&self, input: &[S]) -> FastCryptoResult> { + let mut f1 = self.compute_message_polynomial(input)?.as_vec().clone(); + f1.resize(self.k, S::zero()); + Ok(f1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gao_decoder() { + let a = (1..=7).map(|i| ShareIndex::new(i).unwrap()).collect_vec(); + let k = 3; + let decoder = RSDecoder::new(a.clone(), k); + + let message = vec![S::from(11u128), S::from(22u128), S::from(33u128)]; + let code_word = decoder.encode(message.clone()).unwrap(); + + // Introduce errors + let mut received = code_word.clone(); + received[4] = S::from(20u128); // Error at position 4 + received[2] = S::from(200u128); // Error at position 2 + + let decoded_message = decoder.decode(&received).unwrap(); + assert_eq!(decoded_message, message); + + // Test with too many errors + let mut received = code_word.clone(); + received[4] = S::from(20u128); // Error at position 4 + received[3] = S::from(2000u128); // Error at position 3 + received[2] = S::from(200u128); // Error at position 2 + assert!(decoder.decode(&received).is_err()); + } +} diff --git a/fastcrypto-tbls/src/threshold_schnorr/mod.rs b/fastcrypto-tbls/src/threshold_schnorr/mod.rs index 3e4e1a76c..f48161b16 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/mod.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/mod.rs @@ -33,6 +33,7 @@ pub mod avss; pub mod batch_avss; mod bcs; pub mod complaint; +pub mod gao; pub mod key_derivation; mod pascal_matrix; pub mod presigning; diff --git a/fastcrypto/src/error.rs b/fastcrypto/src/error.rs index 41e0e031e..54d04f6d4 100644 --- a/fastcrypto/src/error.rs +++ b/fastcrypto/src/error.rs @@ -56,6 +56,10 @@ pub enum FastCryptoError { #[error("Out of presigs in the iterator, please create new presigs")] OutOfPresigs, + /// Error in error decoding because there are too many errors to correct. + #[error("Too many errors to correct in error decoding. Up to {0} errors can be corrected.")] + TooManyErrors(usize), + /// General cryptographic error. #[error("General cryptographic error: {0}")] GeneralError(String), From a1df5b0beddfaa4d391e8e2bfe771b01b6000743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 3 Oct 2025 14:44:38 +0200 Subject: [PATCH 06/21] Clean up and reorder --- fastcrypto-tbls/src/polynomial.rs | 211 ++++++++++++++++-------------- 1 file changed, 112 insertions(+), 99 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 606674b18..4eba2006c 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::HashSet; use std::mem::swap; -use std::ops::{Add, AddAssign, Div, Mul, SubAssign}; +use std::ops::{Add, AddAssign, Mul, SubAssign}; /// Types @@ -101,6 +101,18 @@ impl Add<&Poly> for Poly { } } +impl SubAssign> for Poly { + fn sub_assign(&mut self, rhs: Poly) { + if self.0.len() < rhs.0.len() { + self.0.resize(rhs.0.len(), C::zero()); + } + for (a, b) in self.0.iter_mut().zip(&rhs.0) { + *a -= *b; + } + self.reduce(); + } +} + /// GroupElement operations. impl Poly { @@ -109,6 +121,10 @@ impl Poly { Self::from(vec![C::zero()]) } + pub(crate) fn is_zero(&self) -> bool { + self.0 == vec![C::zero()] + } + pub fn one() -> Self { Self::from(vec![C::generator()]) } @@ -249,6 +265,10 @@ impl Poly { pub fn as_vec(&self) -> &Vec { &self.0 } + + fn sum(terms: impl Iterator>) -> Poly { + terms.fold(Poly::zero(), |acc, x| acc + &x) + } } /// Scalar operations. @@ -296,12 +316,12 @@ impl Poly { if !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let x = C::from(index.get() as u128); + let x: C = to_scalar(index); // Convert indices to scalars for interpolation. let indices = points .iter() - .map(|p| C::from(p.index.get() as u128)) + .map(|e| to_scalar(e.index)) .collect::>(); let value = C::sum(indices.iter().enumerate().map(|(j, x_j)| { @@ -325,101 +345,30 @@ impl Poly { if points.is_empty() || !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let result = points - .iter() - .map(|p_j| { - let x_j = C::from(p_j.index.get() as u128); - points - .iter() - .filter(|p_i| p_i.index != p_j.index) - .map(|p_i| { - let x_i = C::from(p_i.index.get() as u128); - Poly::from(vec![-x_i, C::generator()]) - * &(x_j - x_i).inverse().expect("Divisor is never zero") - }) - .fold(Poly::::one(), |product, factor| &product * &factor) - * &p_j.value - }) - .fold(Poly::zero(), |acc, p| acc + &p); - Ok(result) - } -} - -impl Poly { - /// Given exactly `t` polynomial evaluations, it will recover the polynomial's - /// constant term. - pub(crate) fn recover_c0_msm( - t: u16, - shares: impl Iterator>> + Clone, - ) -> Result { - let coeffs = Self::get_lagrange_coefficients_for_c0(t, shares.clone())?; - let plain_shares = shares.map(|s| s.borrow().value).collect::>(); - let res = C::multi_scalar_mul(&coeffs, &plain_shares).expect("sizes match"); - Ok(res) - } -} - -struct Monomial { - coefficient: C, - degree: usize, -} - -impl Div<&Self> for Monomial { - type Output = FastCryptoResult>; - - fn div(self, rhs: &Self) -> Self::Output { - if self.degree < rhs.degree { - return Err(FastCryptoError::InvalidInput); - } - Ok(Monomial { - coefficient: (self.coefficient / rhs.coefficient)?, - degree: self.degree - rhs.degree, - }) - } -} - -impl AddAssign<&Monomial> for Poly { - fn add_assign(&mut self, rhs: &Monomial) { - if self.0.len() <= rhs.degree { - self.0.resize(rhs.degree + 1, C::zero()); - } - self.0[rhs.degree] += rhs.coefficient; - self.reduce(); - } -} - -impl SubAssign> for Poly { - fn sub_assign(&mut self, rhs: Poly) { - if self.0.len() < rhs.0.len() { - self.0.resize(rhs.0.len(), C::zero()); - } - for (a, b) in self.0.iter_mut().zip(&rhs.0) { - *a -= *b; - } - self.reduce(); - } -} - -impl Mul<&Monomial> for &Poly { - type Output = Poly; - - fn mul(self, rhs: &Monomial) -> Poly { - if rhs.coefficient == C::zero() { - return Poly::zero(); - } - let mut result = vec![C::zero(); self.degree() + rhs.degree + 1]; - for (i, coefficient) in self.0.iter().enumerate() { - result[i + rhs.degree] = *coefficient * rhs.coefficient; - } - Poly::from(result) - } -} + let indices = points.iter().map(|e| to_scalar::(e.index)).collect_vec(); -impl Poly { - pub(crate) fn is_zero(&self) -> bool { - self.0 == vec![C::zero()] + Ok(Poly::sum(points.iter().enumerate().map(|(j, p_j)| { + let x_j = indices[j]; + let denominator = C::product( + indices + .iter() + .filter(|x_i| *x_i != &x_j) + .map(|x_i| x_j - x_i), + ) + .inverse(); + let numerator = Poly::product( + indices + .iter() + .enumerate() + .filter(|(i, _)| *i != j) + .map(|(_, x_i)| Poly::from(vec![-*x_i, C::generator()])), + ) * &p_j.value; + numerator * &denominator.expect("Denominator is never zero") + }))) } + /// Returns the leading term of the polynomial. + /// If the polynomial is zero, returns a monomial with coefficient zero and degree zero. fn lead(&self) -> Monomial { if self.is_zero() { return Monomial { @@ -428,7 +377,7 @@ impl Poly { }; } Monomial { - coefficient: *self.0.last().unwrap(), + coefficient: *self.0.last().expect("coefficients are never empty"), degree: self.degree(), } } @@ -441,9 +390,18 @@ impl Poly { } let mut remainder = self.clone(); let mut quotient = Self::zero(); + + let lead_inverse = divisor.lead().coefficient.inverse()?; + + // Function to divide a term by the leading term of the divisor. + // This panics if the degree of the given term is less than that of the divisor. + let divider = |p: Monomial| Monomial { + coefficient: p.coefficient * lead_inverse, + degree: p.degree - divisor.degree(), + }; + while !remainder.is_zero() && remainder.degree() >= divisor.degree() { - // TODO: The divisor lead is always the same, so we can precompute its inverse. - let tmp = (remainder.lead() / &divisor.lead())?; + let tmp = divider(remainder.lead()); quotient += &tmp; remainder -= divisor * &tmp; } @@ -452,7 +410,7 @@ impl Poly { /// Compute the extended GCD of two polynomials. /// Returns (g, x, y, s, t) such that g = self * x + other * y. - /// + /// The loop stops when the degree of g is less than degree_bound. pub fn partial_extended_gcd( &self, other: &Poly, @@ -461,6 +419,7 @@ impl Poly { let mut r = (self.clone(), other.clone()); let mut s = (Poly::one(), Poly::zero()); let mut t = (Poly::zero(), Poly::one()); + while r.0.degree() >= degree_bound && !r.1.is_zero() { let (q, r_new) = r.0.div_rem(&r.1)?; r = (r.1, r_new); @@ -477,4 +436,58 @@ impl Poly { pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { self.partial_extended_gcd(other, 0) } + + fn product(terms: impl Iterator>) -> Poly { + terms.fold(Poly::one(), |acc, x| &acc * &x) + } +} + +impl Poly { + /// Given exactly `t` polynomial evaluations, it will recover the polynomial's + /// constant term. + pub(crate) fn recover_c0_msm( + t: u16, + shares: impl Iterator>> + Clone, + ) -> Result { + let coeffs = Self::get_lagrange_coefficients_for_c0(t, shares.clone())?; + let plain_shares = shares.map(|s| s.borrow().value).collect::>(); + let res = C::multi_scalar_mul(&coeffs, &plain_shares).expect("sizes match"); + Ok(res) + } +} + +#[inline] +fn to_scalar(index: ShareIndex) -> C { + C::from(index.get() as u128) +} + +/// This represents a monomial, e.g., 3 * x^2, where 3 is the coefficient and 2 is the degree. +struct Monomial { + coefficient: C, + degree: usize, +} + +impl AddAssign<&Monomial> for Poly { + fn add_assign(&mut self, rhs: &Monomial) { + if self.0.len() <= rhs.degree { + self.0.resize(rhs.degree + 1, C::zero()); + } + self.0[rhs.degree] += rhs.coefficient; + self.reduce(); + } +} + +impl Mul<&Monomial> for &Poly { + type Output = Poly; + + fn mul(self, rhs: &Monomial) -> Poly { + if rhs.coefficient == C::zero() { + return Poly::zero(); + } + let mut result = vec![C::zero(); self.degree() + rhs.degree + 1]; + for (i, coefficient) in self.0.iter().enumerate() { + result[i + rhs.degree] = *coefficient * rhs.coefficient; + } + Poly::from(result) + } } From e3c053c5d50b7f486fe9529f0e806c63b69b94fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 3 Oct 2025 14:55:50 +0200 Subject: [PATCH 07/21] Clean up stuff in pol module --- fastcrypto-tbls/src/polynomial.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 4eba2006c..f11cf34e0 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -136,7 +136,7 @@ impl Poly { /// Evaluates the polynomial at the specified value. pub fn eval(&self, i: ShareIndex) -> Eval { // Use Horner's Method to evaluate the polynomial. - let xi = C::ScalarType::from(i.get().into()); + let xi: C::ScalarType = to_scalar(i); let res = self .0 .iter() @@ -180,9 +180,8 @@ impl Poly { return Err(FastCryptoError::InvalidInput); } - let full_numerator = indices.iter().fold(C::ScalarType::generator(), |acc, i| { - acc * C::ScalarType::from(*i) - }); + let full_numerator = + C::ScalarType::product(indices.iter().map(|i| C::ScalarType::from(*i))); let mut coeffs = Vec::new(); for i in &indices { @@ -227,10 +226,7 @@ impl Poly { ) -> FastCryptoResult { let coeffs = Self::get_lagrange_coefficients_for_c0(t, shares.clone())?; let plain_shares = shares.map(|s| s.borrow().value); - let res = coeffs - .iter() - .zip(plain_shares) - .fold(C::zero(), |acc, (c, s)| acc + (s * *c)); + let res = C::sum(coeffs.iter().zip(plain_shares).map(|(c, s)| s * c)); Ok(res) } @@ -345,14 +341,14 @@ impl Poly { if points.is_empty() || !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let indices = points.iter().map(|e| to_scalar::(e.index)).collect_vec(); + let indices: Vec = points.iter().map(|e| to_scalar(e.index)).collect_vec(); Ok(Poly::sum(points.iter().enumerate().map(|(j, p_j)| { let x_j = indices[j]; let denominator = C::product( indices .iter() - .filter(|x_i| *x_i != &x_j) + .filter(|&&x_i| x_i != x_j) .map(|x_i| x_j - x_i), ) .inverse(); @@ -360,8 +356,8 @@ impl Poly { indices .iter() .enumerate() - .filter(|(i, _)| *i != j) - .map(|(_, x_i)| Poly::from(vec![-*x_i, C::generator()])), + .filter(|(i, _)| i != &j) + .map(|(_, &x_i)| Poly::from(vec![-x_i, C::generator()])), ) * &p_j.value; numerator * &denominator.expect("Denominator is never zero") }))) From 39d01a17f436a5f3126ecb04dde4bc9dd0b2895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Wed, 8 Oct 2025 21:57:54 -0400 Subject: [PATCH 08/21] Optimise --- fastcrypto-tbls/benches/polynomial.rs | 35 ++++++++- fastcrypto-tbls/src/lib.rs | 1 - fastcrypto-tbls/src/polynomial.rs | 72 ++++++++++++------- fastcrypto-tbls/src/tests/polynomial_tests.rs | 40 +++++------ fastcrypto-tbls/src/threshold_schnorr/gao.rs | 5 +- 5 files changed, 101 insertions(+), 52 deletions(-) diff --git a/fastcrypto-tbls/benches/polynomial.rs b/fastcrypto-tbls/benches/polynomial.rs index 4aa8cab7e..71a6ef657 100644 --- a/fastcrypto-tbls/benches/polynomial.rs +++ b/fastcrypto-tbls/benches/polynomial.rs @@ -9,6 +9,37 @@ use std::num::NonZeroU16; mod polynomial_benches { use super::*; + use fastcrypto_tbls::threshold_schnorr::gao::RSDecoder; + use fastcrypto_tbls::threshold_schnorr::S; + use fastcrypto_tbls::types::ShareIndex; + use itertools::Itertools; + + fn rs_decoder(c: &mut Criterion) { + const SIZES: [usize; 4] = [128, 256, 512, 1024]; + + for n in SIZES { + let k = n / 3; + let a = (1..=n) + .map(|i| ShareIndex::new(i as u16).unwrap()) + .collect_vec(); + let decoder = RSDecoder::new(a.clone(), k); + + let message: Vec = (0..k).map(|i| S::from((i * 10) as u128)).collect(); + let code_word = decoder.encode(message.clone()).unwrap(); + + // Introduce errors + let mut received = code_word.clone(); + received[4] = S::from(20u128); // Error at position 4 + received[2] = S::from(200u128); // Error at position 2 + + let mut rs_decoder: BenchmarkGroup<_> = c.benchmark_group("RS Decoder"); + rs_decoder.bench_function(format!("n={}, k={}", n, k).as_str(), |b| { + b.iter(|| { + decoder.decode(&received).unwrap(); + }) + }); + } + } fn polynomials(c: &mut Criterion) { const SIZES: [usize; 7] = [128, 256, 512, 1024, 2048, 4096, 8192]; @@ -76,8 +107,8 @@ mod polynomial_benches { criterion_group! { name = polynomial_benches; - config = Criterion::default(); - targets = polynomials, + config = Criterion::default().sample_size(10); + targets = polynomials, rs_decoder, } } diff --git a/fastcrypto-tbls/src/lib.rs b/fastcrypto-tbls/src/lib.rs index 74960ddec..00451a4c4 100644 --- a/fastcrypto-tbls/src/lib.rs +++ b/fastcrypto-tbls/src/lib.rs @@ -20,7 +20,6 @@ pub mod nodes; pub mod polynomial; pub mod random_oracle; pub mod tbls; -#[cfg(any(test, feature = "experimental"))] pub mod threshold_schnorr; pub mod types; diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index f11cf34e0..d779f731b 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::HashSet; use std::mem::swap; -use std::ops::{Add, AddAssign, Mul, SubAssign}; +use std::ops::{Add, AddAssign, Div, Mul, MulAssign, SubAssign}; /// Types @@ -343,6 +343,11 @@ impl Poly { } let indices: Vec = points.iter().map(|e| to_scalar(e.index)).collect_vec(); + let mut full_numerator = Poly::one(); + for &x_i in indices.iter() { + full_numerator *= MonicLinear(-x_i); + } + Ok(Poly::sum(points.iter().enumerate().map(|(j, p_j)| { let x_j = indices[j]; let denominator = C::product( @@ -350,16 +355,9 @@ impl Poly { .iter() .filter(|&&x_i| x_i != x_j) .map(|x_i| x_j - x_i), - ) - .inverse(); - let numerator = Poly::product( - indices - .iter() - .enumerate() - .filter(|(i, _)| i != &j) - .map(|(_, &x_i)| Poly::from(vec![-x_i, C::generator()])), - ) * &p_j.value; - numerator * &denominator.expect("Denominator is never zero") + ); + let numerator = &full_numerator / MonicLinear(-x_j); + numerator * &(p_j.value / denominator).unwrap() }))) } @@ -411,30 +409,17 @@ impl Poly { &self, other: &Poly, degree_bound: usize, - ) -> FastCryptoResult<(Poly, Poly, Poly)> { + ) -> FastCryptoResult<(Poly, Poly)> { let mut r = (self.clone(), other.clone()); - let mut s = (Poly::one(), Poly::zero()); let mut t = (Poly::zero(), Poly::one()); while r.0.degree() >= degree_bound && !r.1.is_zero() { let (q, r_new) = r.0.div_rem(&r.1)?; r = (r.1, r_new); - - s.0 -= &q * &s.1; t.0 -= &q * &t.1; - - swap(&mut s.0, &mut s.1); swap(&mut t.0, &mut t.1); } - Ok((r.0, s.0, t.0)) - } - - pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { - self.partial_extended_gcd(other, 0) - } - - fn product(terms: impl Iterator>) -> Poly { - terms.fold(Poly::one(), |acc, x| &acc * &x) + Ok((r.0, t.0)) } } @@ -487,3 +472,38 @@ impl Mul<&Monomial> for &Poly { Poly::from(result) } } + +struct MonicLinear(C); + +impl MulAssign> for Poly { + fn mul_assign(&mut self, rhs: MonicLinear) { + if rhs.0 == C::zero() || self.is_zero() { + *self = Poly::zero(); + return; + } + self.0.push(self.0.last().unwrap().clone()); + for i in (1..self.0.len() - 1).rev() { + self.0[i] = self.0[i] * rhs.0 + self.0[i - 1]; + } + self.0[0] = self.0[0] * rhs.0; + } +} + +impl Div> for &Poly { + type Output = Poly; + + fn div(self, rhs: MonicLinear) -> Poly { + if rhs.0 == C::zero() { + panic!("Division by zero"); + } + if self.is_zero() { + return Poly::zero(); + } + let mut result = self.0.clone(); + for i in (0..self.0.len()).rev().skip(1) { + result[i] = result[i] - &(result[i + 1] * rhs.0); + } + result.remove(0); + Poly::from(result) + } +} diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index f1f71a4db..5aac5f678 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -176,26 +176,26 @@ mod scalar_tests { assert_eq!(lhs, a); } - #[test] - fn test_extended_gcd() { - let mut rng = thread_rng(); - let degree_a = 8; - let degree_b = 5; - let a = crate::polynomial::Poly::from( - iter::from_fn(|| Some(S::rand(&mut rng))) - .take(degree_a + 1) - .collect_vec(), - ); - let b = crate::polynomial::Poly::from( - iter::from_fn(|| Some(S::rand(&mut rng))) - .take(degree_b + 1) - .collect_vec(), - ); - - let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); - - assert_eq!(&x * &a + &(&y * &b), g); - } + // #[test] + // fn test_extended_gcd() { + // let mut rng = thread_rng(); + // let degree_a = 8; + // let degree_b = 5; + // let a = crate::polynomial::Poly::from( + // iter::from_fn(|| Some(S::rand(&mut rng))) + // .take(degree_a + 1) + // .collect_vec(), + // ); + // let b = crate::polynomial::Poly::from( + // iter::from_fn(|| Some(S::rand(&mut rng))) + // .take(degree_b + 1) + // .collect_vec(), + // ); + // + // let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); + // + // assert_eq!(&x * &a + &(&y * &b), g); + // } #[instantiate_tests()] mod ristretto_scalar {} diff --git a/fastcrypto-tbls/src/threshold_schnorr/gao.rs b/fastcrypto-tbls/src/threshold_schnorr/gao.rs index 4a665a882..adf90e4d2 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/gao.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/gao.rs @@ -71,7 +71,7 @@ impl RSDecoder { )?; // Step 2: Partial GCD - let (g, _, v) = Poly::partial_extended_gcd( + let (g, v) = Poly::partial_extended_gcd( &self.g0, &g1, (self.message_length() + self.block_length()) / 2, @@ -87,8 +87,7 @@ impl RSDecoder { /// Encode the message using the Reed-Solomon code defined by the evaluation points `a`. /// Returns an error if the message length is wrong. - #[cfg(test)] - fn encode(&self, message: Vec) -> FastCryptoResult> { + pub fn encode(&self, message: Vec) -> FastCryptoResult> { if message.len() != self.message_length() { return Err(FastCryptoError::InputLengthWrong(self.message_length())); } From 23414bd102d8c5cbfd12e2f7937b3d934274d124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Wed, 8 Oct 2025 22:03:23 -0400 Subject: [PATCH 09/21] clean up --- fastcrypto-tbls/src/polynomial.rs | 15 +++++-- fastcrypto-tbls/src/tests/polynomial_tests.rs | 40 +++++++++---------- fastcrypto-tbls/src/threshold_schnorr/gao.rs | 2 +- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index d779f731b..d59a05d40 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -409,17 +409,24 @@ impl Poly { &self, other: &Poly, degree_bound: usize, - ) -> FastCryptoResult<(Poly, Poly)> { + ) -> FastCryptoResult<(Poly, Poly, Poly)> { let mut r = (self.clone(), other.clone()); + let mut s = (Poly::one(), Poly::zero()); let mut t = (Poly::zero(), Poly::one()); while r.0.degree() >= degree_bound && !r.1.is_zero() { let (q, r_new) = r.0.div_rem(&r.1)?; r = (r.1, r_new); t.0 -= &q * &t.1; + s.0 -= &q * &s.1; swap(&mut t.0, &mut t.1); + swap(&mut s.0, &mut s.1); } - Ok((r.0, t.0)) + Ok((r.0, s.0, t.0)) + } + + pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { + self.partial_extended_gcd(other, 0) } } @@ -481,7 +488,7 @@ impl MulAssign> for Poly { *self = Poly::zero(); return; } - self.0.push(self.0.last().unwrap().clone()); + self.0.push(*self.0.last().unwrap()); for i in (1..self.0.len() - 1).rev() { self.0[i] = self.0[i] * rhs.0 + self.0[i - 1]; } @@ -501,7 +508,7 @@ impl Div> for &Poly { } let mut result = self.0.clone(); for i in (0..self.0.len()).rev().skip(1) { - result[i] = result[i] - &(result[i + 1] * rhs.0); + result[i] = result[i] - result[i + 1] * rhs.0; } result.remove(0); Poly::from(result) diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index 5aac5f678..f1f71a4db 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -176,26 +176,26 @@ mod scalar_tests { assert_eq!(lhs, a); } - // #[test] - // fn test_extended_gcd() { - // let mut rng = thread_rng(); - // let degree_a = 8; - // let degree_b = 5; - // let a = crate::polynomial::Poly::from( - // iter::from_fn(|| Some(S::rand(&mut rng))) - // .take(degree_a + 1) - // .collect_vec(), - // ); - // let b = crate::polynomial::Poly::from( - // iter::from_fn(|| Some(S::rand(&mut rng))) - // .take(degree_b + 1) - // .collect_vec(), - // ); - // - // let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); - // - // assert_eq!(&x * &a + &(&y * &b), g); - // } + #[test] + fn test_extended_gcd() { + let mut rng = thread_rng(); + let degree_a = 8; + let degree_b = 5; + let a = crate::polynomial::Poly::from( + iter::from_fn(|| Some(S::rand(&mut rng))) + .take(degree_a + 1) + .collect_vec(), + ); + let b = crate::polynomial::Poly::from( + iter::from_fn(|| Some(S::rand(&mut rng))) + .take(degree_b + 1) + .collect_vec(), + ); + + let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); + + assert_eq!(&x * &a + &(&y * &b), g); + } #[instantiate_tests()] mod ristretto_scalar {} diff --git a/fastcrypto-tbls/src/threshold_schnorr/gao.rs b/fastcrypto-tbls/src/threshold_schnorr/gao.rs index adf90e4d2..c29354538 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/gao.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/gao.rs @@ -71,7 +71,7 @@ impl RSDecoder { )?; // Step 2: Partial GCD - let (g, v) = Poly::partial_extended_gcd( + let (g, _, v) = Poly::partial_extended_gcd( &self.g0, &g1, (self.message_length() + self.block_length()) / 2, From 8b4efe4b6970e675d5245b3fde29c7951ec149ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 14:47:47 -0400 Subject: [PATCH 10/21] Don't change existing usage --- fastcrypto-tbls/src/polynomial.rs | 33 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index d59a05d40..49e4a3825 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -47,12 +47,7 @@ impl Poly { impl From> for Poly { fn from(c: Vec) -> Self { - if c.is_empty() { - return Self::zero(); - } - let mut p = Self(c); - p.reduce(); - p + Self(c) } } @@ -63,7 +58,6 @@ impl AddAssign<&Self> for Poly { if self.0.len() < other.0.len() { self.0.extend_from_slice(&other.0[self.0.len()..]); } - self.reduce(); } } @@ -71,7 +65,7 @@ impl Mul<&C> for Poly { type Output = Poly; fn mul(self, rhs: &C) -> Self::Output { - Poly::from(self.0.into_iter().map(|c| c * rhs).collect_vec()) + Poly(self.0.into_iter().map(|c| c * rhs).collect()) } } @@ -109,7 +103,6 @@ impl SubAssign> for Poly { for (a, b) in self.0.iter_mut().zip(&rhs.0) { *a -= *b; } - self.reduce(); } } @@ -122,7 +115,7 @@ impl Poly { } pub(crate) fn is_zero(&self) -> bool { - self.0 == vec![C::zero()] + self.0.iter().all(|&c| c == C::zero()) } pub fn one() -> Self { @@ -343,6 +336,7 @@ impl Poly { } let indices: Vec = points.iter().map(|e| to_scalar(e.index)).collect_vec(); + // Compute the full numerator polynomial: (x - x_1)(x - x_2)...(x - x_t) let mut full_numerator = Poly::one(); for &x_i in indices.iter() { full_numerator *= MonicLinear(-x_i); @@ -356,7 +350,8 @@ impl Poly { .filter(|&&x_i| x_i != x_j) .map(|x_i| x_j - x_i), ); - let numerator = &full_numerator / MonicLinear(-x_j); + // Safe since x_j is one of the roots of full_numerator. + let numerator = &full_numerator / &MonicLinear(-x_j); numerator * &(p_j.value / denominator).unwrap() }))) } @@ -398,6 +393,7 @@ impl Poly { let tmp = divider(remainder.lead()); quotient += &tmp; remainder -= divisor * &tmp; + remainder.reduce(); } Ok((quotient, remainder)) } @@ -417,8 +413,11 @@ impl Poly { while r.0.degree() >= degree_bound && !r.1.is_zero() { let (q, r_new) = r.0.div_rem(&r.1)?; r = (r.1, r_new); + r.0.reduce(); + t.0 -= &q * &t.1; s.0 -= &q * &s.1; + swap(&mut t.0, &mut t.1); swap(&mut s.0, &mut s.1); } @@ -461,7 +460,6 @@ impl AddAssign<&Monomial> for Poly { self.0.resize(rhs.degree + 1, C::zero()); } self.0[rhs.degree] += rhs.coefficient; - self.reduce(); } } @@ -480,6 +478,7 @@ impl Mul<&Monomial> for &Poly { } } +/// Represents a monic linear polynomial of the form x + c. struct MonicLinear(C); impl MulAssign> for Poly { @@ -496,21 +495,21 @@ impl MulAssign> for Poly { } } -impl Div> for &Poly { +impl Div<&MonicLinear> for &Poly { type Output = Poly; - fn div(self, rhs: MonicLinear) -> Poly { + fn div(self, rhs: &MonicLinear) -> Poly { if rhs.0 == C::zero() { panic!("Division by zero"); } if self.is_zero() { return Poly::zero(); } - let mut result = self.0.clone(); - for i in (0..self.0.len()).rev().skip(1) { + + let mut result = self.0[1..].to_vec(); + for i in (0..result.len() - 1).rev() { result[i] = result[i] - result[i + 1] * rhs.0; } - result.remove(0); Poly::from(result) } } From 292c3ccc3096101c34302048afa0f38c5861fcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 15:03:09 -0400 Subject: [PATCH 11/21] name --- fastcrypto-tbls/src/dkg_v1.rs | 4 ++-- fastcrypto-tbls/src/dl_verification.rs | 4 ++-- fastcrypto-tbls/src/polynomial.rs | 17 +++++++++++------ fastcrypto-tbls/src/tbls.rs | 4 ++-- fastcrypto-tbls/src/tests/dkg_v1_tests.rs | 2 +- fastcrypto-tbls/src/tests/polynomial_tests.rs | 8 ++++---- fastcrypto-tbls/src/threshold_schnorr/avss.rs | 2 +- .../src/threshold_schnorr/batch_avss.rs | 2 +- fastcrypto-tbls/src/threshold_schnorr/gao.rs | 2 +- 9 files changed, 25 insertions(+), 20 deletions(-) diff --git a/fastcrypto-tbls/src/dkg_v1.rs b/fastcrypto-tbls/src/dkg_v1.rs index 39c38bf12..7b742f689 100644 --- a/fastcrypto-tbls/src/dkg_v1.rs +++ b/fastcrypto-tbls/src/dkg_v1.rs @@ -311,12 +311,12 @@ where return Err(FastCryptoError::InvalidMessage); }; - if self.t as usize != msg.vss_pk.degree() + 1 { + if self.t as usize != msg.vss_pk.degree_bound() + 1 { warn!( "DKG: Message sanity check failed for id {}, expected degree={}, got {}", msg.sender, self.t - 1, - msg.vss_pk.degree() + msg.vss_pk.degree_bound() ); return Err(FastCryptoError::InvalidMessage); } diff --git a/fastcrypto-tbls/src/dl_verification.rs b/fastcrypto-tbls/src/dl_verification.rs index 1e3dd61dc..6995ccb3d 100644 --- a/fastcrypto-tbls/src/dl_verification.rs +++ b/fastcrypto-tbls/src/dl_verification.rs @@ -47,7 +47,7 @@ pub fn verify_poly_evals( poly: &Poly, rng: &mut R, ) -> FastCryptoResult<()> { - assert!(poly.degree() > 0); + assert!(poly.degree_bound() > 0); if evals.is_empty() { return Ok(()); } @@ -59,7 +59,7 @@ pub fn verify_poly_evals( .iter() .map(|e| G::ScalarType::from(e.index.get().into())) .collect::>(); - let coeffs = batch_coefficients(&rs, &evals_as_scalars, poly.degree()); + let coeffs = batch_coefficients(&rs, &evals_as_scalars, poly.degree_bound()); let rhs = G::multi_scalar_mul(&coeffs, poly.as_vec()).expect("sizes match"); if lhs != rhs { diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 49e4a3825..721ff3d4d 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -32,12 +32,16 @@ pub type PublicPoly = Poly; impl Poly { /// Returns the degree of the polynomial - pub fn degree(&self) -> usize { + pub fn degree_bound(&self) -> usize { // e.g. c_0 + c_1 * x + c_2 * x^2 + c_3 * x^3 // ^ 4 coefficients correspond to a 3rd degree poly self.0.len() - 1 } + pub fn degree(&self) -> usize { + self.0.iter().rposition(|&c| c != C::zero()).unwrap_or(0) + } + fn reduce(&mut self) { while self.0.len() > 1 && self.0.last() == Some(&C::zero()) { self.0.pop(); @@ -76,7 +80,7 @@ impl Mul<&Poly> for &Poly { if self.is_zero() || rhs.is_zero() { return Poly::zero(); } - let mut result = vec![C::zero(); self.degree() + rhs.degree() + 1]; + let mut result = vec![C::zero(); self.degree_bound() + rhs.degree_bound() + 1]; for (i, a) in self.0.iter().enumerate() { for (j, b) in rhs.0.iter().enumerate() { result[i + j] += *a * *b; @@ -244,7 +248,7 @@ impl Poly { panic!( "Index out of bounds: requested {}, but polynomial has degree {}", i, - self.degree() + self.degree_bound() ); } &self.0[i] @@ -365,9 +369,10 @@ impl Poly { degree: 0, }; } + let degree = self.degree(); Monomial { - coefficient: *self.0.last().expect("coefficients are never empty"), - degree: self.degree(), + coefficient: self.0[degree], + degree, } } @@ -470,7 +475,7 @@ impl Mul<&Monomial> for &Poly { if rhs.coefficient == C::zero() { return Poly::zero(); } - let mut result = vec![C::zero(); self.degree() + rhs.degree + 1]; + let mut result = vec![C::zero(); self.degree_bound() + rhs.degree + 1]; for (i, coefficient) in self.0.iter().enumerate() { result[i + rhs.degree] = *coefficient * rhs.coefficient; } diff --git a/fastcrypto-tbls/src/tbls.rs b/fastcrypto-tbls/src/tbls.rs index c6d5ef37f..6236f9f9f 100644 --- a/fastcrypto-tbls/src/tbls.rs +++ b/fastcrypto-tbls/src/tbls.rs @@ -71,7 +71,7 @@ pub trait ThresholdBls { partial_sigs: impl Iterator>>, rng: &mut R, ) -> FastCryptoResult<()> { - assert!(vss_pk.degree() > 0 || !msg.is_empty()); + assert!(vss_pk.degree_bound() > 0 || !msg.is_empty()); let (evals_as_scalars, points): (Vec<_>, Vec<_>) = partial_sigs .map(|sig| { let sig = sig.borrow(); @@ -83,7 +83,7 @@ pub trait ThresholdBls { } let rs = get_random_scalars::(points.len(), rng); // TODO: should we cache it instead? that would replace t-wide msm with w-wide msm. - let coeffs = batch_coefficients(&rs, &evals_as_scalars, vss_pk.degree()); + let coeffs = batch_coefficients(&rs, &evals_as_scalars, vss_pk.degree_bound()); let pk = Self::Public::multi_scalar_mul(&coeffs, vss_pk.as_vec()).expect("sizes match"); let aggregated_sig = Self::Signature::multi_scalar_mul(&rs, &points).expect("sizes match"); diff --git a/fastcrypto-tbls/src/tests/dkg_v1_tests.rs b/fastcrypto-tbls/src/tests/dkg_v1_tests.rs index 600a1f22d..9f0ee29d0 100644 --- a/fastcrypto-tbls/src/tests/dkg_v1_tests.rs +++ b/fastcrypto-tbls/src/tests/dkg_v1_tests.rs @@ -621,7 +621,7 @@ fn create_message_generates_valid_message() { assert_eq!(msg.sender, 1); assert_eq!(msg.encrypted_shares.len(), 4); - assert_eq!(msg.vss_pk.degree(), 2); + assert_eq!(msg.vss_pk.degree_bound(), 2); } #[test] diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index f1f71a4db..31048874d 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -25,7 +25,7 @@ mod scalar_tests { fn test_degree() { let s: usize = 5; let p = Poly::::rand(s as u16, &mut thread_rng()); - assert_eq!(p.degree(), s); + assert_eq!(p.degree_bound(), s); } #[test] @@ -169,7 +169,7 @@ mod scalar_tests { ); let (q, r) = a.div_rem(&b).unwrap(); - assert!(r.degree() < b.degree()); + assert!(r.degree_bound() < b.degree_bound()); let mut lhs = &q * &b; lhs += &r; @@ -226,7 +226,7 @@ mod points_tests { let one = G::ScalarType::generator(); let coeff = vec![one, one, one]; let p = Poly::::from(coeff); - assert_eq!(p.degree(), 2); + assert_eq!(p.degree_bound(), 2); let s1 = p.eval(NonZeroU16::new(10).unwrap()); let s2 = p.eval(NonZeroU16::new(20).unwrap()); let s3 = p.eval(NonZeroU16::new(30).unwrap()); @@ -258,7 +258,7 @@ mod points_tests { let one = G::generator(); let coeff = vec![one, one, one]; let p = Poly::::from(coeff); - assert_eq!(p.degree(), 2); + assert_eq!(p.degree_bound(), 2); let s1 = p.eval(NonZeroU16::new(10).unwrap()); let s2 = p.eval(NonZeroU16::new(20).unwrap()); let s3 = p.eval(NonZeroU16::new(30).unwrap()); diff --git a/fastcrypto-tbls/src/threshold_schnorr/avss.rs b/fastcrypto-tbls/src/threshold_schnorr/avss.rs index 2feb69b22..729c651a1 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/avss.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/avss.rs @@ -232,7 +232,7 @@ impl Receiver { /// /// 3. When t+f signatures have been collected in the certificate, the receivers can now verify the certificate and finish the protocol. pub fn process_message(&self, message: &Message) -> FastCryptoResult { - if message.feldman_commitment.degree() != self.t as usize - 1 { + if message.feldman_commitment.degree_bound() != self.t as usize - 1 { return Err(InvalidMessage); } diff --git a/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs b/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs index bd2b4eaa3..1a1b46cdf 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs @@ -343,7 +343,7 @@ impl Receiver { } = message; // The response polynomial should have degree t - 1, but with some negligible probability (if the highest coefficient is zero) it will be smaller. - if response_polynomial.degree() != self.t as usize - 1 { + if response_polynomial.degree_bound() != self.t as usize - 1 { return Err(InvalidMessage); } diff --git a/fastcrypto-tbls/src/threshold_schnorr/gao.rs b/fastcrypto-tbls/src/threshold_schnorr/gao.rs index c29354538..8ee78690d 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/gao.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/gao.rs @@ -79,7 +79,7 @@ impl RSDecoder { // Step 3: Long division let (f1, r) = g.div_rem(&v)?; - if !r.is_zero() || f1.degree() >= self.k { + if !r.is_zero() || f1.degree_bound() >= self.k { return Err(FastCryptoError::TooManyErrors((self.distance() - 1) / 2)); } Ok(f1) From 69a74d74b04fbc4e6b62ccbb6416311b68a7bd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 15:06:04 -0400 Subject: [PATCH 12/21] simplify --- fastcrypto-tbls/src/polynomial.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 721ff3d4d..d1d1120c8 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -43,9 +43,8 @@ impl Poly { } fn reduce(&mut self) { - while self.0.len() > 1 && self.0.last() == Some(&C::zero()) { - self.0.pop(); - } + let degree = self.degree(); + self.0.truncate(degree + 1); } } From 559fdcdca6f16228bad08c7600daf6b393d66222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 15:07:07 -0400 Subject: [PATCH 13/21] revert --- fastcrypto-tbls/src/threshold_schnorr/avss.rs | 2 +- fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcrypto-tbls/src/threshold_schnorr/avss.rs b/fastcrypto-tbls/src/threshold_schnorr/avss.rs index 729c651a1..2feb69b22 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/avss.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/avss.rs @@ -232,7 +232,7 @@ impl Receiver { /// /// 3. When t+f signatures have been collected in the certificate, the receivers can now verify the certificate and finish the protocol. pub fn process_message(&self, message: &Message) -> FastCryptoResult { - if message.feldman_commitment.degree_bound() != self.t as usize - 1 { + if message.feldman_commitment.degree() != self.t as usize - 1 { return Err(InvalidMessage); } diff --git a/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs b/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs index 1a1b46cdf..bd2b4eaa3 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/batch_avss.rs @@ -343,7 +343,7 @@ impl Receiver { } = message; // The response polynomial should have degree t - 1, but with some negligible probability (if the highest coefficient is zero) it will be smaller. - if response_polynomial.degree_bound() != self.t as usize - 1 { + if response_polynomial.degree() != self.t as usize - 1 { return Err(InvalidMessage); } From f642144746a4bb28a86326778e06dd303bf728ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 15:15:49 -0400 Subject: [PATCH 14/21] better naming --- fastcrypto-tbls/src/polynomial.rs | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index d1d1120c8..ee396fb3b 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::HashSet; use std::mem::swap; -use std::ops::{Add, AddAssign, Div, Mul, MulAssign, SubAssign}; +use std::ops::{Add, AddAssign, Mul, MulAssign, SubAssign}; /// Types @@ -354,7 +354,7 @@ impl Poly { .map(|x_i| x_j - x_i), ); // Safe since x_j is one of the roots of full_numerator. - let numerator = &full_numerator / &MonicLinear(-x_j); + let numerator = div_exact(&full_numerator, &MonicLinear(-x_j)); numerator * &(p_j.value / denominator).unwrap() }))) } @@ -499,21 +499,18 @@ impl MulAssign> for Poly { } } -impl Div<&MonicLinear> for &Poly { - type Output = Poly; - - fn div(self, rhs: &MonicLinear) -> Poly { - if rhs.0 == C::zero() { - panic!("Division by zero"); - } - if self.is_zero() { - return Poly::zero(); - } +/// Assuming that `d` divides `n` exactly, return the quotient `n / d`. +fn div_exact(n: &Poly, d: &MonicLinear) -> Poly { + if d.0 == C::zero() { + panic!("Division by zero"); + } + if n.is_zero() { + return Poly::zero(); + } - let mut result = self.0[1..].to_vec(); - for i in (0..result.len() - 1).rev() { - result[i] = result[i] - result[i + 1] * rhs.0; - } - Poly::from(result) + let mut result = n.0[1..].to_vec(); + for i in (0..result.len() - 1).rev() { + result[i] = result[i] - result[i + 1] * d.0; } + Poly::from(result) } From b0d39138c73f1770fbe1734a50810b440184af8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 16:04:26 -0400 Subject: [PATCH 15/21] clean up --- fastcrypto-tbls/benches/polynomial.rs | 2 +- fastcrypto-tbls/src/polynomial.rs | 12 +++++++----- fastcrypto-tbls/src/threshold_schnorr/gao.rs | 12 +++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/fastcrypto-tbls/benches/polynomial.rs b/fastcrypto-tbls/benches/polynomial.rs index 71a6ef657..1cab24b77 100644 --- a/fastcrypto-tbls/benches/polynomial.rs +++ b/fastcrypto-tbls/benches/polynomial.rs @@ -107,7 +107,7 @@ mod polynomial_benches { criterion_group! { name = polynomial_benches; - config = Criterion::default().sample_size(10); + config = Criterion::default(); targets = polynomials, rs_decoder, } } diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index ee396fb3b..611f13cf1 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -31,20 +31,22 @@ pub type PublicPoly = Poly; /// Vector related operations. impl Poly { - /// Returns the degree of the polynomial + /// Returns an upper bound for the degree of the polynomial. + /// The returned number is equal to the size of the underlying coefficient vector - 1. pub fn degree_bound(&self) -> usize { // e.g. c_0 + c_1 * x + c_2 * x^2 + c_3 * x^3 // ^ 4 coefficients correspond to a 3rd degree poly self.0.len() - 1 } + /// Returns the degree of the polynomial, ignoring leading zero coefficients. pub fn degree(&self) -> usize { self.0.iter().rposition(|&c| c != C::zero()).unwrap_or(0) } + /// Removes leading zero coefficients. fn reduce(&mut self) { - let degree = self.degree(); - self.0.truncate(degree + 1); + self.0.truncate(self.degree() + 1); } } @@ -79,7 +81,7 @@ impl Mul<&Poly> for &Poly { if self.is_zero() || rhs.is_zero() { return Poly::zero(); } - let mut result = vec![C::zero(); self.degree_bound() + rhs.degree_bound() + 1]; + let mut result = vec![C::zero(); self.degree() + rhs.degree() + 1]; for (i, a) in self.0.iter().enumerate() { for (j, b) in rhs.0.iter().enumerate() { result[i + j] += *a * *b; @@ -483,7 +485,7 @@ impl Mul<&Monomial> for &Poly { } /// Represents a monic linear polynomial of the form x + c. -struct MonicLinear(C); +pub(crate) struct MonicLinear(pub C); impl MulAssign> for Poly { fn mul_assign(&mut self, rhs: MonicLinear) { diff --git a/fastcrypto-tbls/src/threshold_schnorr/gao.rs b/fastcrypto-tbls/src/threshold_schnorr/gao.rs index 8ee78690d..046438e41 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/gao.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/gao.rs @@ -1,7 +1,7 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::polynomial::{Eval, Poly}; +use crate::polynomial::{Eval, MonicLinear, Poly}; use crate::threshold_schnorr::S; use crate::types::ShareIndex; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; @@ -24,12 +24,10 @@ impl RSDecoder { /// Create a new Gao decoder with the given evaluation points `a` and message length `k`. pub fn new(a: Vec, k: usize) -> Self { assert!(k < a.len(), "Message length must be less than block length"); - let g0 = a - .iter() - .map(|ai| S::from(ai.get() as u128)) - .fold(Poly::one(), |acc, ai| { - &acc * &Poly::from(vec![-ai, S::generator()]) - }); + let mut g0 = Poly::one(); + for ai in &a { + g0 *= MonicLinear(-S::from(ai.get() as u128)); + } Self { g0, a, k } } From 5c839f985b9f8854e32782c7f1278d4691398064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 9 Oct 2025 16:22:23 -0400 Subject: [PATCH 16/21] more clean up --- fastcrypto-tbls/src/polynomial.rs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 611f13cf1..ab3f57c0e 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -32,14 +32,16 @@ pub type PublicPoly = Poly; impl Poly { /// Returns an upper bound for the degree of the polynomial. - /// The returned number is equal to the size of the underlying coefficient vector - 1. + /// The returned number is equal to the size of the underlying coefficient vector - 1, + /// and in case some of the leading elements are zero, the actual degree will be smaller. + /// See also [Poly::degree]. pub fn degree_bound(&self) -> usize { // e.g. c_0 + c_1 * x + c_2 * x^2 + c_3 * x^3 // ^ 4 coefficients correspond to a 3rd degree poly self.0.len() - 1 } - /// Returns the degree of the polynomial, ignoring leading zero coefficients. + /// Returns the degree of the polynomial. pub fn degree(&self) -> usize { self.0.iter().rposition(|&c| c != C::zero()).unwrap_or(0) } @@ -339,25 +341,23 @@ impl Poly { if points.is_empty() || !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let indices: Vec = points.iter().map(|e| to_scalar(e.index)).collect_vec(); + let x: Vec = points.iter().map(|e| to_scalar(e.index)).collect_vec(); // Compute the full numerator polynomial: (x - x_1)(x - x_2)...(x - x_t) let mut full_numerator = Poly::one(); - for &x_i in indices.iter() { - full_numerator *= MonicLinear(-x_i); + for x_i in &x { + full_numerator *= MonicLinear(-*x_i); } Ok(Poly::sum(points.iter().enumerate().map(|(j, p_j)| { - let x_j = indices[j]; let denominator = C::product( - indices - .iter() - .filter(|&&x_i| x_i != x_j) - .map(|x_i| x_j - x_i), + x.iter() + .enumerate() + .filter(|(i, _)| *i != j) + .map(|(_, x_i)| x[j] - x_i), ); - // Safe since x_j is one of the roots of full_numerator. - let numerator = div_exact(&full_numerator, &MonicLinear(-x_j)); - numerator * &(p_j.value / denominator).unwrap() + // Safe since (x - x[j]) divides full_numerator per definition + div_exact(&full_numerator, &MonicLinear(-x[j])) * &(p_j.value / denominator).unwrap() }))) } @@ -503,13 +503,9 @@ impl MulAssign> for Poly { /// Assuming that `d` divides `n` exactly, return the quotient `n / d`. fn div_exact(n: &Poly, d: &MonicLinear) -> Poly { - if d.0 == C::zero() { - panic!("Division by zero"); - } if n.is_zero() { return Poly::zero(); } - let mut result = n.0[1..].to_vec(); for i in (0..result.len() - 1).rev() { result[i] = result[i] - result[i + 1] * d.0; From 902503838b162ee669271c7ae324e64d09c145e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 10 Oct 2025 07:28:50 +0200 Subject: [PATCH 17/21] Eq check for tests --- fastcrypto-tbls/src/polynomial.rs | 7 ++++++- fastcrypto-tbls/src/tests/polynomial_tests.rs | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index ab3f57c0e..02297e4ac 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -431,7 +431,7 @@ impl Poly { } pub fn extended_gcd(&self, other: &Poly) -> FastCryptoResult<(Poly, Poly, Poly)> { - self.partial_extended_gcd(other, 0) + self.partial_extended_gcd(other, 1) } } @@ -512,3 +512,8 @@ fn div_exact(n: &Poly, d: &MonicLinear) -> Poly { } Poly::from(result) } + +#[cfg(test)] +pub(crate) fn poly_eq(a: &Poly, b: &Poly) -> bool { + a.0[..(a.degree() + 1)] == b.0[..(b.degree() + 1)] +} diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index 31048874d..0b67a9d14 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -173,7 +173,7 @@ mod scalar_tests { let mut lhs = &q * &b; lhs += &r; - assert_eq!(lhs, a); + assert!(poly_eq(&lhs, &a)); } #[test] @@ -194,7 +194,7 @@ mod scalar_tests { let (g, x, y) = Poly::extended_gcd(&a, &b).unwrap(); - assert_eq!(&x * &a + &(&y * &b), g); + assert!(poly_eq(&(&x * &a + &(&y * &b)), &g)); } #[instantiate_tests()] From 4cce84372a418eabe712e3dd02817f3bd6ae6aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 10 Oct 2025 08:21:48 +0200 Subject: [PATCH 18/21] clean up --- fastcrypto-tbls/src/polynomial.rs | 23 +++++++++++-------- fastcrypto-tbls/src/tests/polynomial_tests.rs | 2 +- fastcrypto-tbls/src/threshold_schnorr/gao.rs | 18 ++++++--------- fastcrypto-tbls/src/types.rs | 8 ++++++- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 02297e4ac..e083779ec 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -5,6 +5,7 @@ // modified for our needs. // +use crate::types; use crate::types::{IndexedValue, ShareIndex}; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; use fastcrypto::groups::{GroupElement, MultiScalarMul, Scalar}; @@ -136,7 +137,7 @@ impl Poly { /// Evaluates the polynomial at the specified value. pub fn eval(&self, i: ShareIndex) -> Eval { // Use Horner's Method to evaluate the polynomial. - let xi: C::ScalarType = to_scalar(i); + let xi: C::ScalarType = types::to_scalar(i); let res = self .0 .iter() @@ -262,6 +263,10 @@ impl Poly { &self.0 } + pub fn to_vec(self) -> Vec { + self.0 + } + fn sum(terms: impl Iterator>) -> Poly { terms.fold(Poly::zero(), |acc, x| acc + &x) } @@ -312,12 +317,12 @@ impl Poly { if !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let x: C = to_scalar(index); + let x: C = types::to_scalar(index); // Convert indices to scalars for interpolation. let indices = points .iter() - .map(|e| to_scalar(e.index)) + .map(|p| types::to_scalar(p.index)) .collect::>(); let value = C::sum(indices.iter().enumerate().map(|(j, x_j)| { @@ -341,7 +346,10 @@ impl Poly { if points.is_empty() || !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let x: Vec = points.iter().map(|e| to_scalar(e.index)).collect_vec(); + let x: Vec = points + .iter() + .map(|e| types::to_scalar(e.index)) + .collect_vec(); // Compute the full numerator polynomial: (x - x_1)(x - x_2)...(x - x_t) let mut full_numerator = Poly::one(); @@ -449,11 +457,6 @@ impl Poly { } } -#[inline] -fn to_scalar(index: ShareIndex) -> C { - C::from(index.get() as u128) -} - /// This represents a monomial, e.g., 3 * x^2, where 3 is the coefficient and 2 is the degree. struct Monomial { coefficient: C, @@ -501,7 +504,7 @@ impl MulAssign> for Poly { } } -/// Assuming that `d` divides `n` exactly, return the quotient `n / d`. +/// Assuming that `d` divides `n` exactly (or, that `d.0` is a root in `n`), return the quotient `n / d`. fn div_exact(n: &Poly, d: &MonicLinear) -> Poly { if n.is_zero() { return Poly::zero(); diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index 0b67a9d14..20dae572d 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -169,7 +169,7 @@ mod scalar_tests { ); let (q, r) = a.div_rem(&b).unwrap(); - assert!(r.degree_bound() < b.degree_bound()); + assert!(r.degree() < b.degree()); let mut lhs = &q * &b; lhs += &r; diff --git a/fastcrypto-tbls/src/threshold_schnorr/gao.rs b/fastcrypto-tbls/src/threshold_schnorr/gao.rs index 046438e41..1fca7ea1b 100644 --- a/fastcrypto-tbls/src/threshold_schnorr/gao.rs +++ b/fastcrypto-tbls/src/threshold_schnorr/gao.rs @@ -3,9 +3,8 @@ use crate::polynomial::{Eval, MonicLinear, Poly}; use crate::threshold_schnorr::S; -use crate::types::ShareIndex; +use crate::types::{to_scalar, ShareIndex}; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; -use fastcrypto::groups::GroupElement; use itertools::Itertools; /// Decoder for Reed-Solomon codes. @@ -26,7 +25,7 @@ impl RSDecoder { assert!(k < a.len(), "Message length must be less than block length"); let mut g0 = Poly::one(); for ai in &a { - g0 *= MonicLinear(-S::from(ai.get() as u128)); + g0 *= MonicLinear(-to_scalar::(ai)); } Self { g0, a, k } } @@ -61,10 +60,7 @@ impl RSDecoder { .a .iter() .zip(code_word) - .map(|(index, value)| Eval { - index: *index, - value: *value, - }) + .map(|(&index, &value)| Eval { index, value }) .collect_vec(), )?; @@ -77,7 +73,7 @@ impl RSDecoder { // Step 3: Long division let (f1, r) = g.div_rem(&v)?; - if !r.is_zero() || f1.degree_bound() >= self.k { + if !r.is_zero() || f1.degree() >= self.k { return Err(FastCryptoError::TooManyErrors((self.distance() - 1) / 2)); } Ok(f1) @@ -90,14 +86,14 @@ impl RSDecoder { return Err(FastCryptoError::InputLengthWrong(self.message_length())); } let f = Poly::from(message); - Ok(self.a.iter().map(|ai| f.eval(*ai).value).collect_vec()) + Ok(self.a.iter().map(|&ai| f.eval(ai).value).collect_vec()) } /// Try to correct the input and return the decoded message. /// Returns an error if the input length is wrong or if there are too many errors to correct. pub fn decode(&self, input: &[S]) -> FastCryptoResult> { - let mut f1 = self.compute_message_polynomial(input)?.as_vec().clone(); - f1.resize(self.k, S::zero()); + let mut f1 = self.compute_message_polynomial(input)?.to_vec(); + f1.truncate(self.k); Ok(f1) } } diff --git a/fastcrypto-tbls/src/types.rs b/fastcrypto-tbls/src/types.rs index 7c74b5c4a..b5cd42bc4 100644 --- a/fastcrypto-tbls/src/types.rs +++ b/fastcrypto-tbls/src/types.rs @@ -4,8 +4,9 @@ use crate::polynomial::{Eval, PublicPoly}; use crate::tbls; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; -use fastcrypto::groups::{bls12381, GroupElement, HashToGroupElement, Pairing}; +use fastcrypto::groups::{bls12381, GroupElement, HashToGroupElement, Pairing, Scalar}; use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; use std::num::NonZeroU16; /// Implementation of [ThresholdBls] for BLS12-381-min-sig. A variant for BLS12-381-min-pk can be @@ -83,3 +84,8 @@ impl UnindexedValues { Ok(values) } } + +#[inline] +pub(crate) fn to_scalar(index: impl Borrow) -> C { + C::from(index.borrow().get() as u128) +} From 76b8102b7049a3741f792aa30d23b02b06938393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 10 Oct 2025 08:23:29 +0200 Subject: [PATCH 19/21] redundant trait bound --- fastcrypto-tbls/src/polynomial.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index e083779ec..8935106b1 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -53,7 +53,7 @@ impl Poly { } } -impl From> for Poly { +impl From> for Poly { fn from(c: Vec) -> Self { Self(c) } From 29ed3e6de44824ec047dab1aebae7f1ee7c9a542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 10 Oct 2025 08:27:07 +0200 Subject: [PATCH 20/21] clean up --- fastcrypto-tbls/src/polynomial.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 8935106b1..648e85420 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -6,7 +6,7 @@ // use crate::types; -use crate::types::{IndexedValue, ShareIndex}; +use crate::types::{to_scalar, IndexedValue, ShareIndex}; use fastcrypto::error::{FastCryptoError, FastCryptoResult}; use fastcrypto::groups::{GroupElement, MultiScalarMul, Scalar}; use fastcrypto::traits::AllowedRng; @@ -137,7 +137,7 @@ impl Poly { /// Evaluates the polynomial at the specified value. pub fn eval(&self, i: ShareIndex) -> Eval { // Use Horner's Method to evaluate the polynomial. - let xi: C::ScalarType = types::to_scalar(i); + let xi: C::ScalarType = to_scalar(i); let res = self .0 .iter() @@ -268,7 +268,7 @@ impl Poly { } fn sum(terms: impl Iterator>) -> Poly { - terms.fold(Poly::zero(), |acc, x| acc + &x) + terms.fold(Poly::zero(), |sum, term| sum + &term) } } @@ -317,12 +317,12 @@ impl Poly { if !points.iter().map(|p| p.index).all_unique() { return Err(FastCryptoError::InvalidInput); } - let x: C = types::to_scalar(index); + let x: C = to_scalar(index); // Convert indices to scalars for interpolation. let indices = points .iter() - .map(|p| types::to_scalar(p.index)) + .map(|p| to_scalar(p.index)) .collect::>(); let value = C::sum(indices.iter().enumerate().map(|(j, x_j)| { From 01f3d4c094c1b5f2b40540a7a17efaa66d6102c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Fri, 10 Oct 2025 12:43:44 +0200 Subject: [PATCH 21/21] Degree test --- fastcrypto-tbls/src/polynomial.rs | 2 +- fastcrypto-tbls/src/tests/polynomial_tests.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/fastcrypto-tbls/src/polynomial.rs b/fastcrypto-tbls/src/polynomial.rs index 648e85420..df69c64a5 100644 --- a/fastcrypto-tbls/src/polynomial.rs +++ b/fastcrypto-tbls/src/polynomial.rs @@ -48,7 +48,7 @@ impl Poly { } /// Removes leading zero coefficients. - fn reduce(&mut self) { + pub(crate) fn reduce(&mut self) { self.0.truncate(self.degree() + 1); } } diff --git a/fastcrypto-tbls/src/tests/polynomial_tests.rs b/fastcrypto-tbls/src/tests/polynomial_tests.rs index 20dae572d..d9d4272c7 100644 --- a/fastcrypto-tbls/src/tests/polynomial_tests.rs +++ b/fastcrypto-tbls/src/tests/polynomial_tests.rs @@ -22,7 +22,7 @@ mod scalar_tests { use itertools::Itertools; #[test] - fn test_degree() { + fn test_degree_bound() { let s: usize = 5; let p = Poly::::rand(s as u16, &mut thread_rng()); assert_eq!(p.degree_bound(), s); @@ -197,6 +197,17 @@ mod scalar_tests { assert!(poly_eq(&(&x * &a + &(&y * &b)), &g)); } + #[test] + fn test_degree() { + let coefficients = [1, 2, 3, 0, 0].iter().map(|&x| S::from(x)).collect_vec(); + let mut a = crate::polynomial::Poly::from(coefficients); + assert_eq!(a.degree(), 2); + assert_eq!(a.degree_bound(), 4); + a.reduce(); + assert_eq!(a.degree(), 2); + assert_eq!(a.degree_bound(), 2); + } + #[instantiate_tests()] mod ristretto_scalar {}