Skip to main content

confium_tc/
inprocess.rs

1//! In-process synchronous driver for any registered threshold scheme.
2//!
3//! Wraps the multi-round session state machine in a single function call
4//! so language bindings and integration tests don't have to reconstruct
5//! the round / message-routing loop on their own side. The driver is
6//! local — it does no real networking — and is intended for integration
7//! tests, demos, and the in-process surface of language bindings.
8//! Production deployments drive the sessions over a real transport via
9//! `confium-tc-coordinator`.
10//!
11//! ## Why this exists
12//!
13//! Without this module every binding (Ruby, Python, WASM, the in-tree
14//! examples) had to copy the same N-session-create + route-messages +
15//! drive-rounds boilerplate. The CMP20 and GG18 in-process shims were
16//! ~200 LOC each, ~90% of which was identical. This module collapses
17//! that pattern into one place; scheme-specific shims now only declare
18//! their scheme name and how to extract a public key from a share blob.
19//!
20//! ## Output contract
21//!
22//! [`run_dkg`] returns the per-party share blobs produced by
23//! [`Session::result`]. The joint public key is *not* returned — the
24//! caller knows the scheme-specific encoding and extracts it from the
25//! first share (see `confium-tc-cmp20::inprocess::keygen` for the
26//! pattern).
27//!
28//! [`run_sign`] returns the cryptographic artifact (e.g. a 64-byte
29//! `(r, s)` ECDSA signature).
30
31use snafu::ensure;
32
33use crate::Result;
34use crate::error;
35use crate::message::Message;
36use crate::party::{Party, PartyList};
37use crate::session::{Session, SessionParams};
38use crate::share::Share;
39
40/// Upper bound on the number of framework-round iterations before the
41/// driver gives up. Real protocols (FROST, CMP20, GG18) top out at 4;
42/// the bound exists to fail loudly on a misbehaving scheme rather than
43/// spin forever.
44const MAX_ROUNDS: u8 = 8;
45
46/// Drive a registered DKG scheme to completion in-process.
47///
48/// Creates `party_count` sessions (one per party) under the named
49/// scheme, runs every round until all sessions signal completion, and
50/// returns the per-party result blobs in roster order.
51///
52/// `threshold` must be in `1..=party_count`. Parties are in-process
53/// (`Party::inproc`), identified `p0` … `p{n-1}`.
54pub fn run_dkg(scheme: &str, threshold: u32, party_count: usize) -> Result<Vec<Vec<u8>>> {
55    ensure!(party_count > 0, error::EmptyPartyListSnafu {});
56    let roster: Vec<Party> = (0..party_count)
57        .map(|i| Party::inproc(format!("p{i}")))
58        .collect();
59    let parties = PartyList::from_parties(roster);
60    let party_ids: Vec<String> = parties.parties().iter().map(|p| p.id.clone()).collect();
61
62    let mut sessions: Vec<Session> = (0..party_count)
63        .map(|idx| {
64            Session::create(&SessionParams {
65                scheme: scheme.to_string(),
66                parties: parties.clone(),
67                threshold,
68                this_party_idx: idx,
69                local_share: None,
70                message: None,
71            })
72        })
73        .collect::<Result<Vec<_>>>()?;
74
75    drive_to_completion(&mut sessions, &party_ids)?;
76
77    sessions.iter().map(|s| s.result()).collect()
78}
79
80/// Drive a registered signing / decapsulation scheme to completion
81/// in-process using `share_blobs` as the per-party inputs.
82///
83/// One session is created per supplied share. `share_blobs.len()` must
84/// be `>= threshold`. The first session's `result()` is returned — for
85/// honest coalitions every party converges on the same artifact.
86pub fn run_sign(
87    scheme: &str,
88    share_blobs: &[Vec<u8>],
89    threshold: u32,
90    message: &[u8],
91) -> Result<Vec<u8>> {
92    let signer_count = share_blobs.len();
93    ensure!(
94        signer_count as u32 >= threshold,
95        error::ThresholdTooLargeSnafu {
96            threshold,
97            party_count: signer_count,
98        }
99    );
100
101    let roster: Vec<Party> = (0..signer_count)
102        .map(|i| Party::inproc(format!("p{i}")))
103        .collect();
104    let parties = PartyList::from_parties(roster);
105    let party_ids: Vec<String> = parties.parties().iter().map(|p| p.id.clone()).collect();
106
107    let mut sessions: Vec<Session> = (0..signer_count)
108        .map(|idx| {
109            let local_share = Share::new(scheme.to_string(), share_blobs[idx].clone());
110            Session::create(&SessionParams {
111                scheme: scheme.to_string(),
112                parties: parties.clone(),
113                threshold,
114                this_party_idx: idx,
115                local_share: Some(local_share),
116                message: Some(message.to_vec()),
117            })
118        })
119        .collect::<Result<Vec<_>>>()?;
120
121    drive_to_completion(&mut sessions, &party_ids)?;
122
123    sessions[0].result()
124}
125
126/// Drive every session forward until all signal completion or
127/// `MAX_ROUNDS` is exceeded. Messages from one round are routed to
128/// their recipients as the next round's incoming.
129fn drive_to_completion(sessions: &mut [Session], party_ids: &[String]) -> Result<()> {
130    let mut outgoing: Vec<Vec<Message>> = Vec::new();
131    for _round in 1..=MAX_ROUNDS {
132        outgoing = step_rounds(sessions, &outgoing, party_ids)?;
133        if sessions.iter().all(|s| s.is_complete()) {
134            return Ok(());
135        }
136    }
137    Err(error::RoundOverflowSnafu { round: MAX_ROUNDS }.build())
138}
139
140/// Drive one `round_step` per session with routed incoming messages,
141/// returning the per-session outgoing messages for the next round.
142fn step_rounds(
143    sessions: &mut [Session],
144    prev_outgoing: &[Vec<Message>],
145    party_ids: &[String],
146) -> Result<Vec<Vec<Message>>> {
147    let n = sessions.len();
148    let mut incoming: Vec<Vec<Message>> = vec![Vec::new(); n];
149    for (sender_pos, outs) in prev_outgoing.iter().enumerate() {
150        for m in outs {
151            for (recv_pos, pid) in party_ids.iter().enumerate() {
152                if recv_pos == sender_pos {
153                    continue;
154                }
155                if m.is_for(pid) {
156                    incoming[recv_pos].push(m.clone());
157                }
158            }
159        }
160    }
161    let mut next: Vec<Vec<Message>> = Vec::with_capacity(n);
162    for (i, sess) in sessions.iter_mut().enumerate() {
163        let r = sess.round_step(&incoming[i])?;
164        next.push(r.outgoing);
165    }
166    Ok(next)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::party::{Party, PartyList};
173    use crate::registry::{RoundResult, SessionImpl, TcScheme, TcSchemeKind};
174
175    /// A toy two-round DKG scheme used to exercise the driver without
176    /// depending on a real threshold crate. Round 1 broadcasts; round 2
177    /// completes.
178    struct ToyScheme;
179
180    impl TcScheme for ToyScheme {
181        fn name(&self) -> &'static str {
182            "test-inprocess-driver"
183        }
184        fn kind(&self) -> TcSchemeKind {
185            TcSchemeKind::Dkg
186        }
187        fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
188            let id = params.parties.get(params.this_party_idx)?.id.clone();
189            Ok(Box::new(ToySession { id, round_done: 0 }))
190        }
191    }
192
193    struct ToySession {
194        id: String,
195        round_done: u8,
196    }
197
198    impl SessionImpl for ToySession {
199        fn round(&mut self, _incoming: &[Message]) -> Result<RoundResult> {
200            self.round_done += 1;
201            if self.round_done == 1 {
202                Ok(RoundResult::new(
203                    vec![Message::broadcast(&self.id, 1, vec![0xAA])],
204                    false,
205                ))
206            } else {
207                Ok(RoundResult::done())
208            }
209        }
210        fn result(&self) -> Result<Vec<u8>> {
211            Ok(vec![0xAA])
212        }
213        fn destroy(&mut self) {}
214    }
215
216    inventory::submit! {
217        crate::registry::RegisteredScheme {
218            scheme: &ToyScheme as &dyn crate::registry::TcScheme
219        }
220    }
221
222    #[test]
223    fn drive_dkg_two_round_scheme_completes() {
224        let out = run_dkg("test-inprocess-driver", 2, 3).expect("dkg");
225        assert_eq!(out.len(), 3);
226        for blob in &out {
227            assert_eq!(blob, &vec![0xAA]);
228        }
229    }
230
231    #[test]
232    fn drive_dkg_rejects_zero_party_count() {
233        let err = run_dkg("test-inprocess-driver", 0, 0);
234        assert!(err.is_err());
235    }
236
237    #[test]
238    fn drive_sign_unknown_scheme_errors() {
239        let err = run_sign("no-such-scheme", &[vec![1, 2, 3]], 1, b"msg");
240        assert!(err.is_err());
241    }
242
243    #[test]
244    fn drive_sign_below_threshold_errors() {
245        // Three shares claimed, threshold higher than supplied count.
246        let err = run_sign("test-inprocess-driver", &[], 1, b"msg");
247        assert!(err.is_err());
248    }
249
250    fn _ensure_party_list_send_sync(_list: PartyList) {
251        // Compile-time check that the public API stays send + sync as
252        // the framework evolves.
253    }
254
255    fn _ensure_party_send_sync(_p: Party) {}
256}