Skip to main content

confium_tc_core/
registry.rs

1//! Link-time registry of in-process threshold schemes.
2//!
3//! Each TC scheme (FROST-ed25519, GG18-ECDSA-P256, a Pedersen DKG, …)
4//! registers itself with [`register_tc_scheme!`]. The session layer
5//! looks schemes up by name and dispatches lifecycle calls through the
6//! [`TcScheme`] trait.
7//!
8//! This registry is for **in-process** Rust schemes linked into the
9//! same binary as `confium-tc`. Out-of-process plugins (loaded via
10//! `libloading`) advertise the `"tc"` interface name through the core
11//! plugin loader and are dispatched separately — see
12//! `confium-core::ffi::registry`.
13//!
14//! The registry name advertised for plugins is `"tc"` and covers both
15//! `tc-signature` and `tc-kem` via the scheme name; max version 0.
16
17use std::fmt;
18
19use crate::Result;
20use crate::message::Message;
21use crate::session::SessionParams;
22
23/// Broad category of a threshold scheme. Drives which lifecycle entry
24/// points apply (e.g. DKG produces a share on output; signing consumes
25/// a share on input).
26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27pub enum TcSchemeKind {
28    /// Threshold signing — parties cooperatively produce a signature.
29    Signature,
30    /// Threshold key encapsulation — parties cooperatively derive a
31    /// shared secret.
32    Kem,
33    /// Distributed key generation — parties produce a fresh shared
34    /// secret and per-party shares.
35    Dkg,
36}
37
38/// A registered threshold scheme.
39///
40/// Implementations are `Send + Sync` so a scheme can be looked up from
41/// any thread. Each `create_session` call yields a fresh [`SessionImpl`]
42/// that owns the per-session mutable state.
43pub trait TcScheme: Send + Sync {
44    /// Canonical scheme name, e.g. `"FROST-ed25519"`, `"GG18-ECDSA-P256"`.
45    fn name(&self) -> &'static str;
46
47    /// What kind of artifact this scheme produces.
48    fn kind(&self) -> TcSchemeKind;
49
50    /// Build a new session for this scheme. The returned [`SessionImpl`]
51    /// owns all per-session state; the framework drives it via
52    /// [`SessionImpl::round`] / [`SessionImpl::result`] /
53    /// [`SessionImpl::destroy`].
54    fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>>;
55}
56
57/// Per-session scheme state, driven round-by-round by the framework.
58///
59/// A `SessionImpl` is **not** `Sync` — it owns mutable protocol state
60/// for exactly one party's view of one session. The framework moves it
61/// between threads via the owning [`crate::session::Session`] handle.
62pub trait SessionImpl: Send {
63    /// Advance one round. Consumes the messages received since the last
64    /// round and returns the messages to send next, plus a `complete`
65    /// flag indicating the session has produced its output and no more
66    /// rounds are needed.
67    fn round(&mut self, incoming: &[Message]) -> Result<RoundResult>;
68
69    /// Extract the final cryptographic artifact (signature bytes, DKG
70    /// output, KEM shared secret, …). Only valid once a round returned
71    /// `complete == true`.
72    fn result(&self) -> Result<Vec<u8>>;
73
74    /// Release any scheme-owned resources. Called exactly once when the
75    /// framework drops the session. Implementations should zeroize
76    /// sensitive state.
77    fn destroy(&mut self);
78}
79
80/// Outcome of one [`SessionImpl::round`] call.
81#[derive(Debug, Clone, Default)]
82pub struct RoundResult {
83    /// Messages to send for the next round.
84    pub outgoing: Vec<Message>,
85    /// `true` once the protocol is finished and [`SessionImpl::result`]
86    /// is ready to read.
87    pub complete: bool,
88}
89
90impl RoundResult {
91    pub fn new(outgoing: Vec<Message>, complete: bool) -> Self {
92        RoundResult { outgoing, complete }
93    }
94
95    /// Convenience: an empty result that signals the session is done.
96    pub fn done() -> Self {
97        RoundResult {
98            outgoing: Vec::new(),
99            complete: true,
100        }
101    }
102}
103
104/// Wrapper so a `&'static dyn TcScheme` can be collected by `inventory`.
105pub struct RegisteredScheme {
106    pub scheme: &'static dyn TcScheme,
107}
108
109inventory::collect!(RegisteredScheme);
110
111/// Iterator over every scheme registered at link time.
112pub fn iter() -> impl Iterator<Item = &'static dyn TcScheme> {
113    inventory::iter::<RegisteredScheme>().map(|r| r.scheme)
114}
115
116/// Look up a scheme by canonical name. Returns the first match — scheme
117/// name collisions are a registration bug and surface here as
118/// [`crate::error::Error::SchemeNotFound`].
119pub fn find(name: &str) -> Option<&'static dyn TcScheme> {
120    iter().find(|s| s.name() == name)
121}
122
123/// Submit a scheme implementation to the link-time registry.
124///
125/// ```ignore
126/// use confium_tc::register_tc_scheme;
127/// use confium_tc::registry::{TcScheme, TcSchemeKind};
128/// use confium_tc::{SessionImpl, SessionParams};
129/// # use confium_tc::Error;
130/// # struct MyScheme;
131/// # impl TcScheme for MyScheme {
132/// #     fn name(&self) -> &'static str { "demo" }
133/// #     fn kind(&self) -> TcSchemeKind { TcSchemeKind::Signature }
134/// #     fn create_session(&self, _: &SessionParams)
135/// #         -> std::result::Result<Box<dyn SessionImpl>, Error> { unimplemented!() }
136/// # }
137/// register_tc_scheme!(MyScheme);
138/// ```
139#[macro_export]
140macro_rules! register_tc_scheme {
141    ($scheme:ident) => {
142        ::inventory::submit! {
143            $crate::registry::RegisteredScheme { scheme: &$scheme }
144        }
145    };
146}
147
148impl fmt::Debug for dyn TcScheme {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("TcScheme")
151            .field("name", &self.name())
152            .field("kind", &self.kind())
153            .finish()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::error;
161    use crate::session::SessionParams;
162    use crate::share::Share;
163
164    /// A no-op scheme used to exercise the registry + session lifecycle
165    /// end-to-end. It "completes" on the first round and produces a
166    /// fixed result; no real cryptography happens here.
167    struct NoopScheme;
168
169    impl TcScheme for NoopScheme {
170        fn name(&self) -> &'static str {
171            "test-noop"
172        }
173        fn kind(&self) -> TcSchemeKind {
174            TcSchemeKind::Signature
175        }
176        fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
177            Ok(Box::new(NoopSession {
178                result: params
179                    .message
180                    .clone()
181                    .unwrap_or_else(|| b"noop-result".to_vec()),
182                done: false,
183            }))
184        }
185    }
186
187    struct NoopSession {
188        result: Vec<u8>,
189        done: bool,
190    }
191
192    impl SessionImpl for NoopSession {
193        fn round(&mut self, _incoming: &[Message]) -> Result<RoundResult> {
194            self.done = true;
195            Ok(RoundResult::done())
196        }
197        fn result(&self) -> Result<Vec<u8>> {
198            if !self.done {
199                return Err(error::SessionNotCompleteSnafu {}.build());
200            }
201            Ok(self.result.clone())
202        }
203        fn destroy(&mut self) {
204            self.result.fill(0);
205        }
206    }
207
208    // Force the scheme into the link-time registry for these tests.
209    inventory::submit! {
210        RegisteredScheme { scheme: &NoopScheme as &dyn TcScheme }
211    }
212
213    fn make_params() -> SessionParams {
214        use crate::party::{Party, PartyList};
215        SessionParams {
216            scheme: "test-noop".to_string(),
217            parties: PartyList::from_parties(vec![
218                Party::inproc("a"),
219                Party::inproc("b"),
220                Party::inproc("c"),
221            ]),
222            threshold: 2,
223            this_party_idx: 0,
224            local_share: None,
225            message: None,
226        }
227    }
228
229    #[test]
230    fn registry_finds_registered_scheme() {
231        let scheme = find("test-noop").expect("noop scheme must be registered");
232        assert_eq!(scheme.name(), "test-noop");
233        assert_eq!(scheme.kind(), TcSchemeKind::Signature);
234    }
235
236    #[test]
237    fn registry_find_missing_returns_none() {
238        assert!(find("does-not-exist").is_none());
239    }
240
241    #[test]
242    fn scheme_create_session_runs_full_lifecycle() {
243        let scheme = find("test-noop").expect("registered");
244        let params = make_params();
245        let mut session = scheme.create_session(&params).expect("session created");
246
247        let rr = session.round(&[]).expect("round ok");
248        assert!(rr.complete);
249        assert!(rr.outgoing.is_empty());
250
251        let result = session.result().expect("result ok");
252        assert_eq!(result, b"noop-result");
253
254        session.destroy();
255    }
256
257    #[test]
258    fn scheme_create_session_propagates_message_param() {
259        let scheme = find("test-noop").expect("registered");
260        let mut params = make_params();
261        params.message = Some(vec![0x11, 0x22]);
262        let mut session = scheme.create_session(&params).expect("session created");
263        session.round(&[]).expect("round ok");
264        let result = session.result().expect("result ok");
265        assert_eq!(result, vec![0x11, 0x22]);
266    }
267
268    #[test]
269    fn result_before_complete_errors() {
270        let scheme = find("test-noop").expect("registered");
271        let params = make_params();
272        let session = scheme.create_session(&params).expect("session created");
273        let err = session.result().unwrap_err();
274        assert!(matches!(err, error::Error::SessionNotComplete { .. }));
275    }
276
277    #[test]
278    fn share_param_does_not_panic_when_absent() {
279        // Sanity: SessionParams.local_share is optional and unused by
280        // the noop scheme; ensure the field is constructible.
281        let params = make_params();
282        assert!(params.local_share.is_none());
283        let _ = Share::new("test-noop", vec![]);
284    }
285}