Skip to main content

chains_sdk/threshold/frost/
refresh.rs

1//! FROST Proactive Secret Sharing — share refresh without changing the group key.
2//!
3//! Allows participants to refresh their key shares periodically to limit
4//! the window of compromise. After refresh, old shares become useless
5//! but the group public key remains unchanged.
6//!
7//! # Protocol
8//!
9//! Each participant generates a zero-secret polynomial (constant term = 0)
10//! of degree (t-1), evaluates it at each other participant's identifier,
11//! and distributes the "refresh deltas". Each participant adds all received
12//! deltas to their existing share.
13//!
14//! Because the constant term is zero, the group secret `s = f(0)` is unchanged.
15
16use super::keygen::{self, KeyPackage, VssCommitments};
17use crate::error::SignerError;
18use k256::{ProjectivePoint, Scalar};
19use zeroize::Zeroizing;
20
21/// A refresh package from one participant to distribute refresh deltas.
22#[derive(Clone)]
23pub struct RefreshPackage {
24    /// Participant identifier of the sender.
25    pub sender: u16,
26    /// VSS commitments for the zero-secret polynomial.
27    pub commitments: VssCommitments,
28    /// Secret refresh deltas (one per participant, in order).
29    deltas: Vec<Zeroizing<Scalar>>,
30}
31
32impl Drop for RefreshPackage {
33    fn drop(&mut self) {
34        // Zeroizing handles cleanup
35    }
36}
37
38/// Generate a refresh package for proactive share refresh.
39///
40/// Each participant calls this to generate their contribution to the
41/// refresh protocol. The generated polynomial has constant term = 0,
42/// meaning it adds zero to the reconstructed secret.
43///
44/// # Arguments
45/// - `min_signers` — Threshold (t)
46/// - `max_signers` — Total participants (n)
47/// - `my_id` — This participant's identifier
48pub fn generate_refresh(
49    min_signers: u16,
50    max_signers: u16,
51    my_id: u16,
52) -> Result<RefreshPackage, SignerError> {
53    if min_signers < 2 || max_signers < min_signers {
54        return Err(SignerError::ParseError(
55            "refresh requires min >= 2, max >= min".into(),
56        ));
57    }
58
59    // Generate random polynomial with constant term = 0
60    // f(x) = a_1*x + a_2*x^2 + ... + a_{t-1}*x^{t-1}
61    let mut coefficients = vec![Scalar::ZERO]; // a_0 = 0 (preserves group secret)
62    for _ in 1..min_signers {
63        coefficients.push(keygen::random_scalar()?);
64    }
65
66    // VSS commitments: C_k = G * a_k (C_0 = identity since a_0 = 0)
67    let commitment_points = coefficients
68        .iter()
69        .map(|c| (ProjectivePoint::GENERATOR * c).to_affine())
70        .collect();
71
72    // Evaluate polynomial at each participant's identifier
73    let mut deltas = Vec::with_capacity(max_signers as usize);
74    for i in 1..=max_signers {
75        let x = Scalar::from(u64::from(i));
76        let delta = keygen::polynomial_evaluate(&x, &coefficients);
77        deltas.push(Zeroizing::new(delta));
78    }
79
80    Ok(RefreshPackage {
81        sender: my_id,
82        commitments: VssCommitments {
83            commitments: commitment_points,
84        },
85        deltas,
86    })
87}
88
89/// Apply refresh deltas to an existing key package.
90///
91/// Each participant collects their delta from each refresh package
92/// and adds it to their existing secret share.
93///
94/// # Arguments
95/// - `key_package` — The existing key package to refresh
96/// - `refresh_packages` — All refresh packages from all participants
97///
98/// # Returns
99/// A new `KeyPackage` with the refreshed secret share (same group key).
100pub fn apply_refresh(
101    key_package: &KeyPackage,
102    refresh_packages: &[RefreshPackage],
103) -> Result<KeyPackage, SignerError> {
104    let my_idx = (key_package.identifier - 1) as usize;
105
106    // Verify each refresh package's delta against VSS commitments
107    for pkg in refresh_packages {
108        if my_idx >= pkg.deltas.len() {
109            return Err(SignerError::ParseError(
110                "refresh package missing delta".into(),
111            ));
112        }
113        // Verify: the commitment at index 0 should be identity (zero secret)
114        let c0 = pkg.commitments.commitments[0];
115        if ProjectivePoint::from(c0) != ProjectivePoint::IDENTITY {
116            return Err(SignerError::SigningFailed(format!(
117                "refresh package from {} has non-zero constant term",
118                pkg.sender
119            )));
120        }
121        // Verify delta against VSS commitments
122        let valid = pkg
123            .commitments
124            .verify_share(key_package.identifier, &pkg.deltas[my_idx]);
125        if !valid {
126            return Err(SignerError::SigningFailed(format!(
127                "VSS verification failed for refresh from participant {}",
128                pkg.sender
129            )));
130        }
131    }
132
133    // Sum all deltas for this participant
134    let mut delta_sum = Scalar::ZERO;
135    for pkg in refresh_packages {
136        delta_sum += pkg.deltas[my_idx].as_ref();
137    }
138
139    // New share = old share + delta
140    let new_share = *key_package.secret_share() + delta_sum;
141
142    Ok(KeyPackage {
143        identifier: key_package.identifier,
144        secret_share: Zeroizing::new(new_share),
145        group_public_key: key_package.group_public_key,
146        min_participants: key_package.min_participants,
147        max_participants: key_package.max_participants,
148    })
149}
150
151/// Verify that a refresh package is valid (zero-secret invariant).
152///
153/// Checks that the constant term commitment is the identity point,
154/// ensuring the group secret is preserved.
155#[must_use]
156pub fn verify_refresh_package(pkg: &RefreshPackage) -> bool {
157    if pkg.commitments.commitments.is_empty() {
158        return false;
159    }
160    // C_0 must be identity (G * 0)
161    ProjectivePoint::from(pkg.commitments.commitments[0]) == ProjectivePoint::IDENTITY
162}
163
164// ═══════════════════════════════════════════════════════════════════
165// Tests
166// ═══════════════════════════════════════════════════════════════════
167
168#[cfg(test)]
169#[allow(clippy::unwrap_used, clippy::expect_used)]
170mod tests {
171    use super::*;
172    use crate::threshold::frost::signing;
173    use k256::elliptic_curve::sec1::ToEncodedPoint;
174
175    #[test]
176    fn test_refresh_preserves_group_key() {
177        let secret = [0x42u8; 32];
178        let kgen = keygen::trusted_dealer_keygen(&secret, 2, 3).unwrap();
179        let original_gpk = kgen.group_public_key;
180
181        // All 3 participants generate refresh packages
182        let r1 = generate_refresh(2, 3, 1).unwrap();
183        let r2 = generate_refresh(2, 3, 2).unwrap();
184        let r3 = generate_refresh(2, 3, 3).unwrap();
185        let refresh_pkgs = vec![r1, r2, r3];
186
187        // Each participant applies the refresh
188        let new_kp1 = apply_refresh(&kgen.key_packages[0], &refresh_pkgs).unwrap();
189        let new_kp2 = apply_refresh(&kgen.key_packages[1], &refresh_pkgs).unwrap();
190        let new_kp3 = apply_refresh(&kgen.key_packages[2], &refresh_pkgs).unwrap();
191
192        // Group public key must be preserved
193        assert_eq!(new_kp1.group_public_key, original_gpk);
194        assert_eq!(new_kp2.group_public_key, original_gpk);
195
196        // Shares should have changed
197        assert_ne!(
198            *new_kp1.secret_share(),
199            *kgen.key_packages[0].secret_share()
200        );
201    }
202
203    #[test]
204    fn test_refreshed_shares_can_sign() {
205        let secret = [0x42u8; 32];
206        let kgen = keygen::trusted_dealer_keygen(&secret, 2, 3).unwrap();
207        let group_pk = kgen.group_public_key;
208
209        let r1 = generate_refresh(2, 3, 1).unwrap();
210        let r2 = generate_refresh(2, 3, 2).unwrap();
211        let r3 = generate_refresh(2, 3, 3).unwrap();
212        let refresh_pkgs = vec![r1, r2, r3];
213
214        let new_kp1 = apply_refresh(&kgen.key_packages[0], &refresh_pkgs).unwrap();
215        let new_kp2 = apply_refresh(&kgen.key_packages[1], &refresh_pkgs).unwrap();
216
217        // Sign with refreshed shares
218        let msg = b"signing with refreshed shares";
219        let n1 = signing::commit(&new_kp1).unwrap();
220        let n2 = signing::commit(&new_kp2).unwrap();
221        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
222        let s1 = signing::sign(&new_kp1, n1, &comms, msg).unwrap();
223        let s2 = signing::sign(&new_kp2, n2, &comms, msg).unwrap();
224        let sig = signing::aggregate(&comms, &[s1, s2], &group_pk, msg).unwrap();
225
226        assert!(
227            signing::verify(&sig, &group_pk, msg).unwrap(),
228            "refreshed shares must produce valid signatures"
229        );
230    }
231
232    #[test]
233    fn test_refresh_package_verification() {
234        let pkg = generate_refresh(2, 3, 1).unwrap();
235        assert!(verify_refresh_package(&pkg));
236    }
237
238    #[test]
239    fn test_refresh_invalid_params() {
240        assert!(generate_refresh(1, 3, 1).is_err());
241        assert!(generate_refresh(4, 3, 1).is_err());
242    }
243
244    #[test]
245    fn test_multiple_refreshes() {
246        let secret = [0x42u8; 32];
247        let kgen = keygen::trusted_dealer_keygen(&secret, 2, 3).unwrap();
248        let original_gpk = kgen.group_public_key;
249
250        // First refresh
251        let r1 = vec![
252            generate_refresh(2, 3, 1).unwrap(),
253            generate_refresh(2, 3, 2).unwrap(),
254            generate_refresh(2, 3, 3).unwrap(),
255        ];
256        let kp1_r1 = apply_refresh(&kgen.key_packages[0], &r1).unwrap();
257        let kp2_r1 = apply_refresh(&kgen.key_packages[1], &r1).unwrap();
258
259        // Second refresh
260        let r2 = vec![
261            generate_refresh(2, 3, 1).unwrap(),
262            generate_refresh(2, 3, 2).unwrap(),
263            generate_refresh(2, 3, 3).unwrap(),
264        ];
265        let kp1_r2 = apply_refresh(&kp1_r1, &r2).unwrap();
266        let kp2_r2 = apply_refresh(&kp2_r1, &r2).unwrap();
267
268        // Still the same group key
269        assert_eq!(kp1_r2.group_public_key, original_gpk);
270
271        // Can still sign
272        let msg = b"after two refreshes";
273        let n1 = signing::commit(&kp1_r2).unwrap();
274        let n2 = signing::commit(&kp2_r2).unwrap();
275        let comms = vec![n1.commitments.clone(), n2.commitments.clone()];
276        let s1 = signing::sign(&kp1_r2, n1, &comms, msg).unwrap();
277        let s2 = signing::sign(&kp2_r2, n2, &comms, msg).unwrap();
278        let sig = signing::aggregate(&comms, &[s1, s2], &original_gpk, msg).unwrap();
279        assert!(signing::verify(&sig, &original_gpk, msg).unwrap());
280    }
281}