Skip to main content

yip_ca/
roast_coordinator.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use frost_ed25519 as frost;
4
5use crate::rpc::NonceSlotId;
6
7/// Active concurrent FROST signing session.
8pub 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
15/// ROAST Coordinator (Ruffing et al., CCS 2022) wrapping preprocessed FROST(Ed25519, SHA-512).
16/// Automatically spawns parallel sessions with available signers so up to n-t stalling or
17/// malicious signers cannot delay certificate issuance.
18pub struct RoastCoordinator {
19    pub threshold: usize,
20    pub pubkey_package: frost::keys::PublicKeyPackage,
21    pub message: Vec<u8>,
22    /// Signers currently available (not busy in an in-flight session).
23    pub responsive_signers: BTreeSet<frost::Identifier>,
24    /// Preprocessed commitments per signer.
25    pub commitment_queues:
26        BTreeMap<frost::Identifier, VecDeque<(NonceSlotId, frost::round1::SigningCommitments)>>,
27    /// Active concurrent sessions.
28    pub active_sessions: BTreeMap<u128, RoastSession>,
29    /// Maps a busy signer to the session_id it is currently serving.
30    pub signer_to_session: BTreeMap<frost::Identifier, u128>,
31    /// Malicious signers detected via verify_signature_share failure.
32    pub banished_signers: BTreeSet<frost::Identifier>,
33    next_session_id: u128,
34}
35
36impl RoastCoordinator {
37    /// Creates a fresh ROAST coordinator for a target message buffer.
38    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    /// Registers a batch of preprocessed commitments from a signer and marks it responsive.
57    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    /// Attempts to spawn a new parallel FROST session if `responsive_signers` has at least `threshold`
73    /// signers with available preprocessed commitments.
74    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    /// Receives and verifies a partial SignatureShare from `signer_id`.
136    /// Immediately isolates culprits via `verify_signature_share`, returns honest signers
137    /// to the responsive pool, and completes signing as soon as any session gathers `threshold` shares.
138    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        // Identifiable abort: verify this individual signer's share
161        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        // Replenish commitment and return signer to responsive set
174        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}