Skip to main content

confium_coordinator/coordinator/
frost_integration.rs

1//! FROST coordinator integration — wire FROST into the coordinator.
2
3use crate::coordinator::coordinator::Coordinator;
4use crate::coordinator::session::{SessionId, SessionRequest};
5use serde::{Deserialize, Serialize};
6
7/// FROST signing scheme type.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum FrostScheme {
10    Ed25519,
11    P256,
12}
13
14/// FROST session metadata.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct FrostSession {
17    pub session_id: SessionId,
18    pub scheme: FrostScheme,
19    pub threshold: u32,
20    pub party_count: u32,
21    pub message_hash_hex: String,
22    pub partial_signatures: Vec<PartialFrostSig>,
23}
24
25/// A partial FROST signature from one party.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PartialFrostSig {
28    pub signer_id: String,
29    pub party_idx: u32,
30    pub partial_sig_hex: String,
31}
32
33impl FrostSession {
34    pub fn new(
35        session_id: &str,
36        scheme: FrostScheme,
37        threshold: u32,
38        party_count: u32,
39        message: &[u8],
40    ) -> Self {
41        Self {
42            session_id: session_id.into(),
43            scheme,
44            threshold,
45            party_count,
46            message_hash_hex: hex::encode(message),
47            partial_signatures: Vec::new(),
48        }
49    }
50
51    pub fn submit_partial(&mut self, partial: PartialFrostSig) -> Result<(), String> {
52        if partial.party_idx == 0 || partial.party_idx > self.party_count {
53            return Err("invalid party index".into());
54        }
55        if self
56            .partial_signatures
57            .iter()
58            .any(|s| s.party_idx == partial.party_idx)
59        {
60            return Err("duplicate partial signature".into());
61        }
62        self.partial_signatures.push(partial);
63        Ok(())
64    }
65
66    pub fn is_ready(&self) -> bool {
67        self.partial_signatures.len() >= self.threshold as usize
68    }
69
70    pub fn missing_parties(&self) -> Vec<u32> {
71        (1..=self.party_count)
72            .filter(|i| !self.partial_signatures.iter().any(|s| s.party_idx == *i))
73            .collect()
74    }
75
76    pub fn ordered_partials(&self) -> Vec<&PartialFrostSig> {
77        let mut partials = self.partial_signatures.iter().collect::<Vec<_>>();
78        partials.sort_by_key(|p| p.party_idx);
79        partials
80    }
81}
82
83/// Create a FROST signing session on the coordinator.
84pub fn create_frost_session(
85    coordinator: &mut Coordinator,
86    scheme: FrostScheme,
87    quorum_id: &str,
88    threshold: u32,
89    party_count: u32,
90    message: &[u8],
91) -> Result<(SessionId, FrostSession), String> {
92    let scheme_name = match scheme {
93        FrostScheme::Ed25519 => "FROST-ed25519",
94        FrostScheme::P256 => "FROST-P256",
95    };
96    let request = SessionRequest {
97        quorum_id: quorum_id.into(),
98        scheme: scheme_name.into(),
99        message: message.to_vec(),
100        threshold,
101        num_parties: party_count,
102        unlock_window_minutes: 60,
103        requested_by: "frost-integration".into(),
104    };
105    let session_id = coordinator
106        .create_session(request)
107        .map_err(|e| format!("{e:?}"))?;
108    let frost_session = FrostSession::new(&session_id, scheme, threshold, party_count, message);
109    Ok((session_id, frost_session))
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn frost_session_new() {
118        let session = FrostSession::new("s1", FrostScheme::P256, 2, 3, b"message");
119        assert_eq!(session.threshold, 2);
120        assert_eq!(session.party_count, 3);
121        assert!(!session.is_ready());
122    }
123
124    #[test]
125    fn submit_partial_increments() {
126        let mut session = FrostSession::new("s1", FrostScheme::Ed25519, 2, 3, b"msg");
127        session
128            .submit_partial(PartialFrostSig {
129                signer_id: "a".into(),
130                party_idx: 1,
131                partial_sig_hex: "abc".into(),
132            })
133            .unwrap();
134        assert_eq!(session.partial_signatures.len(), 1);
135    }
136
137    #[test]
138    fn ready_at_threshold() {
139        let mut session = FrostSession::new("s1", FrostScheme::P256, 2, 3, b"msg");
140        session
141            .submit_partial(PartialFrostSig {
142                signer_id: "a".into(),
143                party_idx: 1,
144                partial_sig_hex: "aa".into(),
145            })
146            .unwrap();
147        session
148            .submit_partial(PartialFrostSig {
149                signer_id: "b".into(),
150                party_idx: 2,
151                partial_sig_hex: "bb".into(),
152            })
153            .unwrap();
154        assert!(session.is_ready());
155    }
156
157    #[test]
158    fn duplicate_partial_rejected() {
159        let mut session = FrostSession::new("s1", FrostScheme::P256, 2, 3, b"msg");
160        session
161            .submit_partial(PartialFrostSig {
162                signer_id: "a".into(),
163                party_idx: 1,
164                partial_sig_hex: "aa".into(),
165            })
166            .unwrap();
167        assert!(
168            session
169                .submit_partial(PartialFrostSig {
170                    signer_id: "a".into(),
171                    party_idx: 1,
172                    partial_sig_hex: "aa".into(),
173                })
174                .is_err()
175        );
176    }
177
178    #[test]
179    fn invalid_party_idx_rejected() {
180        let mut session = FrostSession::new("s1", FrostScheme::P256, 2, 3, b"msg");
181        assert!(
182            session
183                .submit_partial(PartialFrostSig {
184                    signer_id: "x".into(),
185                    party_idx: 0,
186                    partial_sig_hex: "x".into(),
187                })
188                .is_err()
189        );
190        assert!(
191            session
192                .submit_partial(PartialFrostSig {
193                    signer_id: "x".into(),
194                    party_idx: 99,
195                    partial_sig_hex: "x".into(),
196                })
197                .is_err()
198        );
199    }
200
201    #[test]
202    fn missing_parties_listed() {
203        let mut session = FrostSession::new("s1", FrostScheme::P256, 3, 5, b"msg");
204        session
205            .submit_partial(PartialFrostSig {
206                signer_id: "a".into(),
207                party_idx: 1,
208                partial_sig_hex: "a".into(),
209            })
210            .unwrap();
211        session
212            .submit_partial(PartialFrostSig {
213                signer_id: "c".into(),
214                party_idx: 3,
215                partial_sig_hex: "c".into(),
216            })
217            .unwrap();
218        assert_eq!(session.missing_parties(), vec![2, 4, 5]);
219    }
220
221    #[test]
222    fn ordered_partials_sorted() {
223        let mut session = FrostSession::new("s1", FrostScheme::P256, 3, 5, b"msg");
224        session
225            .submit_partial(PartialFrostSig {
226                signer_id: "c".into(),
227                party_idx: 3,
228                partial_sig_hex: "c".into(),
229            })
230            .unwrap();
231        session
232            .submit_partial(PartialFrostSig {
233                signer_id: "a".into(),
234                party_idx: 1,
235                partial_sig_hex: "a".into(),
236            })
237            .unwrap();
238        let ordered = session.ordered_partials();
239        assert_eq!(ordered[0].party_idx, 1);
240        assert_eq!(ordered[1].party_idx, 3);
241    }
242
243    #[test]
244    fn create_frost_session_on_coordinator() {
245        let mut coord = Coordinator::new();
246        let (sid, frost) =
247            create_frost_session(&mut coord, FrostScheme::P256, "quorum-1", 2, 3, b"hello")
248                .unwrap();
249        assert!(!sid.is_empty());
250        assert_eq!(frost.scheme, FrostScheme::P256);
251        assert_eq!(coord.session_count(), 1);
252    }
253
254    #[test]
255    fn frost_session_serializes() {
256        let session = FrostSession::new("s1", FrostScheme::Ed25519, 2, 3, b"msg");
257        let json = serde_json::to_string(&session).unwrap();
258        assert!(json.contains("Ed25519"));
259    }
260}