Skip to main content

confium_tc_frost_ed25519/
polynomial.rs

1//! Polynomial helpers for verifiable secret sharing.
2//!
3//! FROST's DKG and signing both rest on Shamir-style secret sharing over
4//! the scalar field: a secret `a_0` is committed as the constant term of
5//! a degree-`T-1` polynomial `f(X) = a_0 + a_1·X + … + a_{T-1}·X^{T-1}`,
6//! and party `i`'s share is `f(i)`. [`lagrange_coefficient`] rebuilds the
7//! original secret (or any linear function of it) from any `T` shares via
8//! interpolation, without ever recombining the shares themselves.
9
10use curve25519_dalek::scalar::Scalar;
11use curve25519_dalek::traits::Identity;
12
13use crate::group;
14
15/// Compute the Lagrange coefficient `λ_i` for party `i` relative to the
16/// participating set `S` (party indices, all distinct). The coefficient is
17///
18/// ```text
19///   λ_i = ∏_{j ∈ S, j ≠ i}  j / (j - i)
20/// ```
21///
22/// evaluated in the scalar field mod ℓ. Used both during DKG (to weight
23/// per-party VSS contributions) and during signing (to weight share
24/// responses).
25///
26/// Panics if `i` is not in `participants`. Callers must ensure the roster
27/// is well-formed before calling.
28pub fn lagrange_coefficient(i: u32, participants: &[u32]) -> Scalar {
29    let mut num = Scalar::ONE;
30    let mut den = Scalar::ONE;
31    let i_scalar = Scalar::from(i);
32    for &j in participants {
33        if j == i {
34            continue;
35        }
36        let j_scalar = Scalar::from(j);
37        num *= j_scalar;
38        den *= j_scalar - i_scalar;
39    }
40    num * den.invert()
41}
42
43/// A degree-`(t-1)` polynomial over the scalar field used for VSS.
44/// Coefficients are little-endian: `f(X) = sum coeff[k] * X^k`.
45pub struct Polynomial {
46    coeff: Vec<Scalar>,
47}
48
49impl Polynomial {
50    /// Build a polynomial from its coefficient vector. `coeff[0]` is the
51    /// constant term (the committed secret for a VSS polynomial).
52    pub fn from_coefficients(coeff: Vec<Scalar>) -> Self {
53        debug_assert!(!coeff.is_empty(), "polynomial must have at least one term");
54        Polynomial { coeff }
55    }
56
57    /// Number of coefficients — equal to the threshold `T` for a degree
58    /// `T-1` polynomial.
59    pub fn degree_plus_one(&self) -> usize {
60        self.coeff.len()
61    }
62
63    /// The constant term `f(0)` — the committed secret.
64    pub fn constant(&self) -> Scalar {
65        self.coeff[0]
66    }
67
68    /// Borrow the coefficient vector.
69    pub fn coefficients(&self) -> &[Scalar] {
70        &self.coeff
71    }
72
73    /// Evaluate `f(x)` at a party index using Horner's rule.
74    pub fn evaluate(&self, x: u32) -> Scalar {
75        let x_scalar = Scalar::from(x);
76        let mut acc = *self.coeff.last().expect("non-empty polynomial");
77        for c in self.coeff[..self.coeff.len() - 1].iter().rev() {
78            acc = acc * x_scalar + c;
79        }
80        acc
81    }
82}
83
84/// A Feldman-style commitment list to a VSS polynomial: `C_k = a_k · B`
85/// for each coefficient `a_k`. Reveal nothing about the secret beyond what
86/// the public key already does (since `C_0 = A`, the public key), but let
87/// recipients verify that a share `f(i)` is consistent with the
88/// committed polynomial via `f(i)·B == Σ_k i^k · C_k`.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct CommitmentList {
91    /// `C_k = a_k · B`, encoded as 32-byte compressed points.
92    commits: Vec<[u8; group::ELEMENT_BYTES]>,
93}
94
95impl CommitmentList {
96    /// Build from already-encoded commitment bytes.
97    pub fn from_bytes(commits: Vec<[u8; group::ELEMENT_BYTES]>) -> Self {
98        CommitmentList { commits }
99    }
100
101    /// Build by committing each coefficient of `poly` to the base point.
102    pub fn commit(poly: &Polynomial) -> Self {
103        let commits = poly
104            .coefficients()
105            .iter()
106            .map(|a| group::point_to_bytes(&group::mul_base(a)))
107            .collect();
108        CommitmentList { commits }
109    }
110
111    /// The aggregate public key `A = C_0 = a_0 · B` as compressed bytes.
112    pub fn public_key_bytes(&self) -> [u8; group::ELEMENT_BYTES] {
113        self.commits[0]
114    }
115
116    /// The full commitment list as compressed-point bytes.
117    pub fn as_bytes(&self) -> &[[u8; group::ELEMENT_BYTES]] {
118        &self.commits
119    }
120
121    /// Verify a share `s_i` claimed to be `f(i)` for the polynomial this
122    /// list commits to. Returns `true` iff `s_i · B == Σ_k i^k · C_k`.
123    pub fn verify_share(&self, participant: u32, share: &Scalar) -> bool {
124        let lhs = group::mul_base(share);
125        // Compute Σ_k i^k · C_k via Horner on the decompressed commitments.
126        // Walk coefficients high → low so we can fold in powers of i.
127        let i_scalar = Scalar::from(participant);
128        let mut acc = curve25519_dalek::edwards::EdwardsPoint::identity();
129        for c_bytes in self.commits.iter().rev() {
130            let c = match group::point_from_bytes(c_bytes) {
131                Some(p) => p,
132                None => return false,
133            };
134            acc = acc * i_scalar + c;
135        }
136        acc == lhs
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use curve25519_dalek::scalar::Scalar;
144
145    #[test]
146    fn lagrange_of_single_party_is_one() {
147        let lambda = lagrange_coefficient(1, &[1]);
148        assert_eq!(lambda, Scalar::ONE);
149    }
150
151    #[test]
152    fn lagrange_coefficients_recover_secret() {
153        // f(X) = a0 + a1·X with a0 = 7, a1 = 3. Shares: f(1) = 10, f(2) = 13.
154        let a0 = Scalar::from(7u64);
155        let a1 = Scalar::from(3u64);
156        let poly = Polynomial::from_coefficients(vec![a0, a1]);
157        let s1 = poly.evaluate(1);
158        let s2 = poly.evaluate(2);
159        assert_eq!(s1, Scalar::from(10u64));
160        assert_eq!(s2, Scalar::from(13u64));
161        // Recover a0 from {1, 2} via Lagrange.
162        let l1 = lagrange_coefficient(1, &[1, 2]);
163        let l2 = lagrange_coefficient(2, &[1, 2]);
164        let recovered = s1 * l1 + s2 * l2;
165        assert_eq!(recovered, a0);
166    }
167
168    #[test]
169    fn polynomial_evaluate_matches_definition() {
170        let coeff = vec![Scalar::from(1u64), Scalar::from(2u64), Scalar::from(3u64)];
171        let poly = Polynomial::from_coefficients(coeff);
172        // f(5) = 1 + 2·5 + 3·25 = 1 + 10 + 75 = 86
173        let got = poly.evaluate(5);
174        let want = Scalar::from(86u64);
175        assert_eq!(got, want);
176    }
177
178    #[test]
179    fn commitment_list_verifies_valid_share() {
180        let poly = Polynomial::from_coefficients(vec![
181            Scalar::from(11u64),
182            Scalar::from(7u64),
183            Scalar::from(3u64),
184        ]);
185        let cl = CommitmentList::commit(&poly);
186        // party 2's share
187        let s2 = poly.evaluate(2);
188        assert!(cl.verify_share(2, &s2));
189        // wrong share fails
190        let bad = s2 + Scalar::ONE;
191        assert!(!cl.verify_share(2, &bad));
192    }
193
194    #[test]
195    fn commitment_list_public_key_matches_constant_term() {
196        let poly = Polynomial::from_coefficients(vec![Scalar::from(42u64), Scalar::from(9u64)]);
197        let cl = CommitmentList::commit(&poly);
198        let want = group::point_to_bytes(&group::mul_base(&Scalar::from(42u64)));
199        assert_eq!(cl.public_key_bytes(), want);
200    }
201}