1use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashSet;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum SigningRound {
19 Round1,
21 Round2,
23 Round3,
25 Round4,
27 Completed,
29 Aborted,
31}
32
33impl SigningRound {
34 pub fn next(&self) -> Option<Self> {
36 match self {
37 Self::Round1 => Some(Self::Round2),
38 Self::Round2 => Some(Self::Round3),
39 Self::Round3 => Some(Self::Round4),
40 Self::Round4 => Some(Self::Completed),
41 Self::Completed | Self::Aborted => None,
42 }
43 }
44
45 pub fn number(&self) -> u32 {
47 match self {
48 Self::Round1 => 1,
49 Self::Round2 => 2,
50 Self::Round3 => 3,
51 Self::Round4 => 4,
52 Self::Completed => 0,
53 Self::Aborted => 0,
54 }
55 }
56
57 pub fn is_terminal(&self) -> bool {
59 matches!(self, Self::Completed | Self::Aborted)
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct RoundState {
66 pub round: SigningRound,
68 pub responded: HashSet<String>,
70 pub started_at: DateTime<Utc>,
72 pub round_data: Vec<(String, Vec<u8>)>,
74}
75
76impl RoundState {
77 fn new(round: SigningRound) -> Self {
78 Self {
79 round,
80 responded: HashSet::new(),
81 started_at: Utc::now(),
82 round_data: Vec::new(),
83 }
84 }
85}
86
87#[derive(Debug, thiserror::Error)]
89pub enum RoundError {
90 #[error(
92 "signer {signer} submitted in round {:?}, expected {:?}",
93 actual,
94 expected
95 )]
96 WrongRound {
97 signer: String,
99 actual: SigningRound,
101 expected: SigningRound,
103 },
104 #[error("signer {0} already responded in this round")]
106 DuplicateResponse(String),
107 #[error("protocol already completed")]
109 AlreadyCompleted,
110 #[error("protocol aborted: {0}")]
112 Aborted(String),
113 #[error("not enough responses: {have}/{need}")]
115 InsufficientResponses { have: usize, need: usize },
116}
117
118pub struct RoundCoordinator {
121 threshold: u32,
122 party_count: u32,
123 current: RoundState,
124 history: Vec<RoundState>,
125}
126
127impl RoundCoordinator {
128 pub fn new(threshold: u32, party_count: u32) -> Self {
130 Self {
131 threshold,
132 party_count,
133 current: RoundState::new(SigningRound::Round1),
134 history: Vec::new(),
135 }
136 }
137
138 pub fn current_round(&self) -> SigningRound {
140 self.current.round
141 }
142
143 pub fn response_count(&self) -> usize {
145 self.current.responded.len()
146 }
147
148 pub fn has_responded(&self, signer_id: &str) -> bool {
150 self.current.responded.contains(signer_id)
151 }
152
153 pub fn submit(
156 &mut self,
157 signer_id: &str,
158 data: Vec<u8>,
159 ) -> Result<Option<SigningRound>, RoundError> {
160 if self.current.round.is_terminal() {
161 return Err(RoundError::AlreadyCompleted);
162 }
163 if self.current.responded.contains(signer_id) {
164 return Err(RoundError::DuplicateResponse(signer_id.into()));
165 }
166 self.current.responded.insert(signer_id.into());
167 self.current.round_data.push((signer_id.into(), data));
168
169 if self.current.responded.len() >= self.threshold as usize {
170 let next = self
171 .current
172 .round
173 .next()
174 .ok_or(RoundError::AlreadyCompleted)?;
175 let old = std::mem::replace(&mut self.current, RoundState::new(next));
176 self.history.push(old);
177 return Ok(Some(next));
178 }
179 Ok(None)
180 }
181
182 pub fn round_data(&self, round: SigningRound) -> Vec<(String, Vec<u8>)> {
184 if round == self.current.round {
185 return self.current.round_data.clone();
186 }
187 self.history
188 .iter()
189 .find(|s| s.round == round)
190 .map(|s| s.round_data.clone())
191 .unwrap_or_default()
192 }
193
194 pub fn rounds_completed(&self) -> usize {
196 self.history.len()
197 }
198
199 pub fn abort(&mut self, reason: &str) {
201 self.current = RoundState::new(SigningRound::Aborted);
202 let _ = reason;
203 }
204
205 pub fn force_advance(&mut self) -> Option<SigningRound> {
208 let next = self.current.round.next()?;
209 let old = std::mem::replace(&mut self.current, RoundState::new(next));
210 self.history.push(old);
211 Some(next)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn starts_in_round_1() {
221 let rc = RoundCoordinator::new(2, 3);
222 assert_eq!(rc.current_round(), SigningRound::Round1);
223 }
224
225 #[test]
226 fn submit_advances_at_threshold() {
227 let mut rc = RoundCoordinator::new(2, 3);
228 assert!(rc.submit("alice", vec![1]).unwrap().is_none());
229 assert_eq!(rc.current_round(), SigningRound::Round1);
230 let advanced = rc.submit("bob", vec![2]).unwrap();
231 assert_eq!(advanced, Some(SigningRound::Round2));
232 assert_eq!(rc.current_round(), SigningRound::Round2);
233 }
234
235 #[test]
236 fn duplicate_response_rejected() {
237 let mut rc = RoundCoordinator::new(2, 3);
238 rc.submit("alice", vec![1]).unwrap();
239 assert!(matches!(
240 rc.submit("alice", vec![2]),
241 Err(RoundError::DuplicateResponse(_))
242 ));
243 }
244
245 #[test]
246 fn full_protocol_progression() {
247 let mut rc = RoundCoordinator::new(2, 3);
248 for round in [
249 SigningRound::Round1,
250 SigningRound::Round2,
251 SigningRound::Round3,
252 ] {
253 assert_eq!(rc.current_round(), round);
254 rc.submit("alice", vec![0xAA]).unwrap();
255 rc.submit("bob", vec![0xBB]).unwrap();
256 }
257 assert_eq!(rc.current_round(), SigningRound::Round4);
258 rc.submit("alice", vec![]).unwrap();
259 rc.submit("bob", vec![]).unwrap();
260 assert_eq!(rc.current_round(), SigningRound::Completed);
261 }
262
263 #[test]
264 fn completed_rejects_submissions() {
265 let mut rc = RoundCoordinator::new(1, 1);
266 rc.submit("alice", vec![]).unwrap();
267 assert_eq!(rc.current_round(), SigningRound::Round2);
268 rc.force_advance();
269 rc.force_advance();
270 rc.force_advance();
271 assert_eq!(rc.current_round(), SigningRound::Completed);
272 assert!(rc.submit("alice", vec![]).is_err());
273 }
274
275 #[test]
276 fn round_data_collected() {
277 let mut rc = RoundCoordinator::new(2, 3);
278 rc.submit("alice", vec![0x11]).unwrap();
279 rc.submit("bob", vec![0x22]).unwrap();
280 let r1_data = rc.round_data(SigningRound::Round1);
281 assert_eq!(r1_data.len(), 2);
282 }
283
284 #[test]
285 fn force_advance_skips_threshold() {
286 let mut rc = RoundCoordinator::new(3, 5);
287 rc.submit("alice", vec![]).unwrap();
288 rc.force_advance();
289 assert_eq!(rc.current_round(), SigningRound::Round2);
290 }
291
292 #[test]
293 fn rounds_completed_tracks_history() {
294 let mut rc = RoundCoordinator::new(2, 3);
295 assert_eq!(rc.rounds_completed(), 0);
296 rc.submit("a", vec![]).unwrap();
297 rc.submit("b", vec![]).unwrap();
298 assert_eq!(rc.rounds_completed(), 1);
299 }
300
301 #[test]
302 fn abort_sets_terminal_state() {
303 let mut rc = RoundCoordinator::new(2, 3);
304 rc.abort("test failure");
305 assert_eq!(rc.current_round(), SigningRound::Aborted);
306 assert!(rc.current_round().is_terminal());
307 }
308
309 #[test]
310 fn round_number_correct() {
311 assert_eq!(SigningRound::Round1.number(), 1);
312 assert_eq!(SigningRound::Round4.number(), 4);
313 assert_eq!(SigningRound::Completed.number(), 0);
314 }
315
316 #[test]
317 fn terminal_states_have_no_next() {
318 assert!(SigningRound::Completed.next().is_none());
319 assert!(SigningRound::Aborted.next().is_none());
320 }
321
322 #[test]
323 fn has_responded_tracks_signers() {
324 let mut rc = RoundCoordinator::new(2, 3);
325 rc.submit("alice", vec![]).unwrap();
326 assert!(rc.has_responded("alice"));
327 assert!(!rc.has_responded("bob"));
328 }
329}