confium_coordinator/coordinator/
frost_integration.rs1use crate::coordinator::coordinator::Coordinator;
4use crate::coordinator::session::{SessionId, SessionRequest};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum FrostScheme {
10 Ed25519,
11 P256,
12}
13
14#[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#[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
83pub 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}