yip_ca/
roast_coordinator.rs1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use frost_ed25519 as frost;
4
5use crate::rpc::NonceSlotId;
6
7pub struct RoastSession {
9 pub session_id: u128,
10 pub participants: BTreeSet<frost::Identifier>,
11 pub signing_package: frost::SigningPackage,
12 pub collected_shares: BTreeMap<frost::Identifier, frost::round2::SignatureShare>,
13}
14
15pub struct RoastCoordinator {
19 pub threshold: usize,
20 pub pubkey_package: frost::keys::PublicKeyPackage,
21 pub message: Vec<u8>,
22 pub responsive_signers: BTreeSet<frost::Identifier>,
24 pub commitment_queues:
26 BTreeMap<frost::Identifier, VecDeque<(NonceSlotId, frost::round1::SigningCommitments)>>,
27 pub active_sessions: BTreeMap<u128, RoastSession>,
29 pub signer_to_session: BTreeMap<frost::Identifier, u128>,
31 pub banished_signers: BTreeSet<frost::Identifier>,
33 next_session_id: u128,
34}
35
36impl RoastCoordinator {
37 pub fn new(
39 threshold: usize,
40 pubkey_package: frost::keys::PublicKeyPackage,
41 message: Vec<u8>,
42 ) -> Self {
43 Self {
44 threshold,
45 pubkey_package,
46 message,
47 responsive_signers: BTreeSet::new(),
48 commitment_queues: BTreeMap::new(),
49 active_sessions: BTreeMap::new(),
50 signer_to_session: BTreeMap::new(),
51 banished_signers: BTreeSet::new(),
52 next_session_id: 1,
53 }
54 }
55
56 pub fn register_commitments(
58 &mut self,
59 signer_id: frost::Identifier,
60 commitments: Vec<(NonceSlotId, frost::round1::SigningCommitments)>,
61 ) {
62 if self.banished_signers.contains(&signer_id) {
63 return;
64 }
65 let queue = self.commitment_queues.entry(signer_id).or_default();
66 queue.extend(commitments);
67 if !self.signer_to_session.contains_key(&signer_id) {
68 self.responsive_signers.insert(signer_id);
69 }
70 }
71
72 pub fn try_spawn_session(
75 &mut self,
76 ) -> Option<(
77 u128,
78 frost::SigningPackage,
79 BTreeMap<frost::Identifier, NonceSlotId>,
80 )> {
81 let eligible: Vec<frost::Identifier> = self
82 .responsive_signers
83 .iter()
84 .copied()
85 .filter(|id| {
86 !self.banished_signers.contains(id)
87 && self
88 .commitment_queues
89 .get(id)
90 .is_some_and(|q| !q.is_empty())
91 })
92 .take(self.threshold)
93 .collect();
94
95 if eligible.len() < self.threshold {
96 return None;
97 }
98
99 let session_id = self.next_session_id;
100 self.next_session_id = self.next_session_id.checked_add(1).unwrap();
101
102 let mut commitments_map = BTreeMap::new();
103 let mut slot_map = BTreeMap::new();
104 let mut participants = BTreeSet::new();
105
106 for id in eligible {
107 self.responsive_signers.remove(&id);
108 self.signer_to_session.insert(id, session_id);
109 let (slot_id, comm) = self
110 .commitment_queues
111 .get_mut(&id)
112 .expect("queue exists")
113 .pop_front()
114 .expect("queue not empty");
115 commitments_map.insert(id, comm);
116 slot_map.insert(id, slot_id);
117 participants.insert(id);
118 }
119
120 let signing_package = frost::SigningPackage::new(commitments_map, &self.message);
121
122 self.active_sessions.insert(
123 session_id,
124 RoastSession {
125 session_id,
126 participants,
127 signing_package: signing_package.clone(),
128 collected_shares: BTreeMap::new(),
129 },
130 );
131
132 Some((session_id, signing_package, slot_map))
133 }
134
135 pub fn receive_share(
139 &mut self,
140 session_id: u128,
141 signer_id: frost::Identifier,
142 share: frost::round2::SignatureShare,
143 replenish: Option<(NonceSlotId, frost::round1::SigningCommitments)>,
144 ) -> Result<Option<frost::Signature>, frost::Error> {
145 let session = match self.active_sessions.get_mut(&session_id) {
146 Some(s) => s,
147 None => return Ok(None),
148 };
149
150 if !session.participants.contains(&signer_id) {
151 return Ok(None);
152 }
153
154 let verifying_share = self
155 .pubkey_package
156 .verifying_shares()
157 .get(&signer_id)
158 .ok_or(frost::Error::UnknownIdentifier)?;
159
160 if let Err(err) = frost_core::verify_signature_share(
162 signer_id,
163 verifying_share,
164 &share,
165 &session.signing_package,
166 self.pubkey_package.verifying_key(),
167 ) {
168 self.banished_signers.insert(signer_id);
169 self.signer_to_session.remove(&signer_id);
170 return Err(err);
171 }
172
173 if let Some(rep) = replenish {
175 self.commitment_queues
176 .entry(signer_id)
177 .or_default()
178 .push_back(rep);
179 }
180 self.signer_to_session.remove(&signer_id);
181 self.responsive_signers.insert(signer_id);
182
183 session.collected_shares.insert(signer_id, share);
184
185 if session.collected_shares.len() == self.threshold {
186 let final_sig = frost::aggregate(
187 &session.signing_package,
188 &session.collected_shares,
189 &self.pubkey_package,
190 )?;
191 return Ok(Some(final_sig));
192 }
193
194 Ok(None)
195 }
196}