Skip to main content

confium_tc_core/
session.rs

1//! Session lifecycle: create, round, result, destroy.
2//!
3//! A [`Session`] owns the per-party state of one threshold protocol
4//! run. The framework drives it round-by-round: feed in the messages
5//! received from peers, get back the messages to send, repeat until a
6//! round signals `complete`, then read the [`Session::result`].
7//!
8//! The session delegates all scheme-specific work to a [`SessionImpl`]
9//! produced by the registered [`crate::registry::TcScheme`]. The
10//! framework layer above this is transport-agnostic — see
11//! `TODO.roadmap/05-networking-primitives.md` for how [`crate::message::Message`]s
12//! get moved between parties.
13
14use snafu::ensure;
15
16use crate::Result;
17use crate::error;
18use crate::message::Message;
19use crate::party::PartyList;
20use crate::registry::{self, RoundResult, SessionImpl, TcSchemeKind};
21use crate::share::Share;
22
23/// Parameters handed to [`Session::create`].
24///
25/// `message` is the per-session input artifact: the message to sign for
26/// `Signature` schemes, the ciphertext to decapsulate for `Kem`
27/// schemes, or `None` for `Dkg` schemes (which have no external input).
28#[derive(Debug, Clone)]
29pub struct SessionParams {
30    /// Canonical scheme name, e.g. `"FROST-ed25519"`.
31    pub scheme: String,
32    /// Ordered roster of all N parties.
33    pub parties: PartyList,
34    /// Threshold T — minimum cooperating party count.
35    pub threshold: u32,
36    /// Index into `parties` identifying which party we are.
37    pub this_party_idx: usize,
38    /// Pre-existing share (for signing / decapsulation sessions). `None`
39    /// for DKG sessions that produce a share on output.
40    pub local_share: Option<Share>,
41    /// External input to the session — the message to sign, the
42    /// ciphertext to decapsulate, etc.
43    pub message: Option<Vec<u8>>,
44}
45
46/// One party's view of one threshold protocol run.
47///
48/// Owns the [`SessionImpl`] produced by the scheme and the bookkeeping
49/// the framework needs to validate round calls.
50pub struct Session {
51    scheme_name: String,
52    scheme_kind: TcSchemeKind,
53    threshold: u32,
54    this_party_idx: usize,
55    party_count: usize,
56    round: u8,
57    complete: bool,
58    impl_: Box<dyn SessionImpl>,
59}
60
61impl std::fmt::Debug for Session {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("Session")
64            .field("scheme_name", &self.scheme_name)
65            .field("scheme_kind", &self.scheme_kind)
66            .field("threshold", &self.threshold)
67            .field("this_party_idx", &self.this_party_idx)
68            .field("party_count", &self.party_count)
69            .field("round", &self.round)
70            .field("complete", &self.complete)
71            .finish_non_exhaustive()
72    }
73}
74
75impl Session {
76    /// Resolve `params.scheme` against the link-time registry and build
77    /// a fresh session. Validates the roster + threshold + index before
78    /// handing control to the scheme.
79    pub fn create(params: &SessionParams) -> Result<Self> {
80        params.parties.validate(params.threshold)?;
81        ensure!(
82            params.this_party_idx < params.parties.len(),
83            error::ThisPartyIdxOutOfRangeSnafu {
84                idx: params.this_party_idx,
85                party_count: params.parties.len(),
86            }
87        );
88        if let Some(share) = &params.local_share {
89            share.assert_scheme(&params.scheme)?;
90        }
91
92        let scheme = registry::find(&params.scheme).ok_or_else(|| {
93            error::SchemeNotFoundSnafu {
94                name: params.scheme.clone(),
95            }
96            .build()
97        })?;
98        let impl_ = scheme.create_session(params)?;
99        Ok(Session {
100            scheme_name: scheme.name().to_string(),
101            scheme_kind: scheme.kind(),
102            threshold: params.threshold,
103            this_party_idx: params.this_party_idx,
104            party_count: params.parties.len(),
105            round: 0,
106            complete: false,
107            impl_,
108        })
109    }
110
111    pub fn scheme_name(&self) -> &str {
112        &self.scheme_name
113    }
114
115    pub fn scheme_kind(&self) -> TcSchemeKind {
116        self.scheme_kind
117    }
118
119    pub fn threshold(&self) -> u32 {
120        self.threshold
121    }
122
123    pub fn this_party_idx(&self) -> usize {
124        self.this_party_idx
125    }
126
127    pub fn party_count(&self) -> usize {
128        self.party_count
129    }
130
131    /// Current round number. Starts at 0 (no rounds run yet); after the
132    /// first [`Session::round`] call it is 1.
133    pub fn round(&self) -> u8 {
134        self.round
135    }
136
137    pub fn is_complete(&self) -> bool {
138        self.complete
139    }
140
141    /// Step the session forward one round.
142    ///
143    /// `incoming` is the set of [`Message`]s this party received since
144    /// the last round (from all peers). Returns the messages this party
145    /// needs to send next. Once a round returns `complete == true`,
146    /// [`Session::result`] is ready and further `round` calls error.
147    pub fn round_step(&mut self, incoming: &[Message]) -> Result<RoundResult> {
148        ensure!(!self.complete, error::SessionAlreadyCompleteSnafu {});
149        self.round = self
150            .round
151            .checked_add(1)
152            .ok_or_else(|| error::RoundOverflowSnafu { round: self.round }.build())?;
153        let res = self.impl_.round(incoming)?;
154        if res.complete {
155            self.complete = true;
156        }
157        Ok(res)
158    }
159
160    /// Read the final cryptographic artifact. Errors until a round has
161    /// signaled completion.
162    pub fn result(&self) -> Result<Vec<u8>> {
163        ensure!(self.complete, error::SessionNotCompleteSnafu {});
164        self.impl_.result()
165    }
166
167    /// For DKG sessions: extract the per-party share and the shared
168    /// public key the protocol produced. The default implementation
169    /// delegates to [`SessionImpl::result`] for the public-key bytes and
170    /// returns `None` for the share unless the scheme overrides this via
171    /// its own protocol — the framework reads the share through the
172    /// scheme plugin's DKG-specific entry point in a later iteration.
173    ///
174    /// For the skeleton this is a thin wrapper: `result()` yields the
175    /// shared public key; the share is produced by the scheme and read
176    /// back via the FFI `cfm_tc_dkg_output_share` entry point.
177    pub fn dkg_public_key(&self) -> Result<Vec<u8>> {
178        ensure!(self.complete, error::SessionNotCompleteSnafu {});
179        ensure!(
180            self.scheme_kind == TcSchemeKind::Dkg,
181            error::NotADkgSessionSnafu {
182                kind: self.scheme_kind,
183            }
184        );
185        self.impl_.result()
186    }
187
188    /// Release scheme-owned resources. After `destroy` the session must
189    /// not be used again.
190    pub fn destroy(&mut self) {
191        self.impl_.destroy();
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::party::{Party, PartyList};
199
200    /// A minimal in-test scheme so the session lifecycle can be
201    /// exercised without depending on the registry-test scheme. Two
202    /// rounds: round 1 echoes a broadcast, round 2 completes.
203    struct TwoRoundScheme;
204
205    impl crate::registry::TcScheme for TwoRoundScheme {
206        fn name(&self) -> &'static str {
207            "test-two-round"
208        }
209        fn kind(&self) -> TcSchemeKind {
210            TcSchemeKind::Signature
211        }
212        fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
213            let our_id = params.parties.get(params.this_party_idx)?.id.clone();
214            Ok(Box::new(TwoRoundSession {
215                our_id,
216                message: params.message.clone().unwrap_or_default(),
217                round_done: 0,
218            }))
219        }
220    }
221
222    struct TwoRoundSession {
223        our_id: String,
224        message: Vec<u8>,
225        round_done: u8,
226    }
227
228    impl SessionImpl for TwoRoundSession {
229        fn round(&mut self, _incoming: &[Message]) -> Result<RoundResult> {
230            self.round_done += 1;
231            if self.round_done == 1 {
232                let msg = Message::broadcast(&self.our_id, 1, self.message.clone());
233                Ok(RoundResult::new(vec![msg], false))
234            } else {
235                Ok(RoundResult::done())
236            }
237        }
238        fn result(&self) -> Result<Vec<u8>> {
239            Ok(self.message.clone())
240        }
241        fn destroy(&mut self) {
242            self.message.fill(0);
243        }
244    }
245
246    // Register the test scheme at link time.
247    inventory::submit! {
248        crate::registry::RegisteredScheme {
249            scheme: &TwoRoundScheme as &dyn crate::registry::TcScheme
250        }
251    }
252
253    fn params(scheme: &str, idx: usize, threshold: u32) -> SessionParams {
254        SessionParams {
255            scheme: scheme.to_string(),
256            parties: PartyList::from_parties(vec![
257                Party::inproc("a"),
258                Party::inproc("b"),
259                Party::inproc("c"),
260            ]),
261            threshold,
262            this_party_idx: idx,
263            local_share: None,
264            message: Some(b"hello".to_vec()),
265        }
266    }
267
268    #[test]
269    fn session_create_resolves_registered_scheme() {
270        let params = params("test-two-round", 0, 2);
271        let session = Session::create(&params).expect("session created");
272        assert_eq!(session.scheme_name(), "test-two-round");
273        assert_eq!(session.scheme_kind(), TcSchemeKind::Signature);
274        assert_eq!(session.threshold(), 2);
275        assert_eq!(session.this_party_idx(), 0);
276        assert_eq!(session.party_count(), 3);
277        assert_eq!(session.round(), 0);
278        assert!(!session.is_complete());
279    }
280
281    #[test]
282    fn session_create_unknown_scheme_errors() {
283        let mut params = params("test-two-round", 0, 2);
284        params.scheme = "no-such-scheme".to_string();
285        let err = Session::create(&params).unwrap_err();
286        assert!(matches!(err, error::Error::SchemeNotFound { .. }));
287    }
288
289    #[test]
290    fn session_create_rejects_bad_party_index() {
291        let params = params("test-two-round", 99, 2);
292        let err = Session::create(&params).unwrap_err();
293        assert!(matches!(
294            err,
295            error::Error::ThisPartyIdxOutOfRange {
296                idx: 99,
297                party_count: 3,
298                ..
299            }
300        ));
301    }
302
303    #[test]
304    fn session_create_rejects_threshold_above_party_count() {
305        let params = params("test-two-round", 0, 99);
306        let err = Session::create(&params).unwrap_err();
307        assert!(matches!(err, error::Error::ThresholdTooLarge { .. }));
308    }
309
310    #[test]
311    fn session_create_rejects_share_scheme_mismatch() {
312        let mut params = params("test-two-round", 0, 2);
313        params.local_share = Some(Share::new("wrong-scheme", vec![1]));
314        let err = Session::create(&params).unwrap_err();
315        assert!(matches!(err, error::Error::ShareSchemeMismatch { .. }));
316    }
317
318    #[test]
319    fn session_round_progresses_then_completes() {
320        let params = params("test-two-round", 0, 2);
321        let mut session = Session::create(&params).expect("session");
322
323        // Round 1: no incoming (first round), one outgoing broadcast.
324        let r1 = session.round_step(&[]).expect("round 1");
325        assert!(!r1.complete);
326        assert_eq!(r1.outgoing.len(), 1);
327        assert!(r1.outgoing[0].is_broadcast());
328        assert_eq!(session.round(), 1);
329
330        // Round 2: completes.
331        let r2 = session.round_step(&[]).expect("round 2");
332        assert!(r2.complete);
333        assert!(session.is_complete());
334
335        let result = session.result().expect("result");
336        assert_eq!(result, b"hello");
337    }
338
339    #[test]
340    fn session_round_after_complete_errors() {
341        let params = params("test-two-round", 0, 2);
342        let mut session = Session::create(&params).expect("session");
343        session.round_step(&[]).expect("round 1");
344        session.round_step(&[]).expect("round 2 completes");
345        let err = session.round_step(&[]).unwrap_err();
346        assert!(matches!(err, error::Error::SessionAlreadyComplete { .. }));
347    }
348
349    #[test]
350    fn session_result_before_complete_errors() {
351        let params = params("test-two-round", 0, 2);
352        let session = Session::create(&params).expect("session");
353        let err = session.result().unwrap_err();
354        assert!(matches!(err, error::Error::SessionNotComplete { .. }));
355    }
356
357    #[test]
358    fn session_dkg_public_key_rejects_non_dkg() {
359        let params = params("test-two-round", 0, 2);
360        let mut session = Session::create(&params).expect("session");
361        session.round_step(&[]).expect("round 1");
362        session.round_step(&[]).expect("round 2 completes");
363        let err = session.dkg_public_key().unwrap_err();
364        assert!(matches!(err, error::Error::NotADkgSession { .. }));
365    }
366}