Skip to main content

confium_tc_core/
party.rs

1//! Party identity and the roster of participants in a threshold session.
2//!
3//! A threshold protocol runs between N parties. Each party has a stable
4//! identifier (a short ASCII handle, opaque to the framework) and, for
5//! networked sessions, a transport endpoint URL. The framework itself
6//! treats party IDs as uninterpreted strings — scheme-specific encoding
7//! (e.g. a numeric index, a public-key fingerprint) is the plugin's job.
8//!
9//! See `TODO.roadmap/04-threshold-cryptography.md` for the design and
10//! `TODO.roadmap/05-networking-primitives.md` for the transport contract.
11
12use std::fmt;
13
14use snafu::ensure;
15
16use crate::Result;
17use crate::error;
18
19/// One participant in a threshold session.
20///
21/// `id` is the canonical ASCII name used as `from_party_id` /
22/// `to_party_id` in [`crate::message::Message`]. `transport_endpoint` is
23/// a URL the Network pillar can connect to (e.g.
24/// `quic://node1.example.com:443`); it may be `None` for in-process
25/// sessions where the framework routes messages directly.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Party {
28    pub id: String,
29    pub transport_endpoint: Option<String>,
30}
31
32impl Party {
33    pub fn new(id: impl Into<String>, transport_endpoint: Option<String>) -> Self {
34        Party {
35            id: id.into(),
36            transport_endpoint,
37        }
38    }
39
40    /// Build a party with no network endpoint — useful for in-process
41    /// simulation harnesses.
42    pub fn inproc(id: impl Into<String>) -> Self {
43        Party {
44            id: id.into(),
45            transport_endpoint: None,
46        }
47    }
48}
49
50/// Ordered roster of parties for a session.
51///
52/// The ordering defines each party's index, which `cfm_tc_session_create`
53/// receives as `this_party_idx`. The threshold T is tracked separately on
54/// [`crate::session::Session`] — a [`PartyList`] is purely the roster.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct PartyList {
57    parties: Vec<Party>,
58}
59
60impl PartyList {
61    pub fn new() -> Self {
62        PartyList {
63            parties: Vec::new(),
64        }
65    }
66
67    pub fn from_parties(parties: Vec<Party>) -> Self {
68        PartyList { parties }
69    }
70
71    pub fn parties(&self) -> &[Party] {
72        &self.parties
73    }
74
75    pub fn len(&self) -> usize {
76        self.parties.len()
77    }
78
79    pub fn is_empty(&self) -> bool {
80        self.parties.is_empty()
81    }
82
83    pub fn push(&mut self, party: Party) {
84        self.parties.push(party);
85    }
86
87    /// Fetch the party at `idx`, returning [`error::Error::PartyIndexOutOfRange`]
88    /// when out of bounds.
89    pub fn get(&self, idx: usize) -> Result<&Party> {
90        self.parties.get(idx).ok_or_else(|| {
91            error::PartyIndexOutOfRangeSnafu {
92                idx,
93                count: self.parties.len(),
94            }
95            .build()
96        })
97    }
98
99    /// Look up a party by canonical id.
100    pub fn find(&self, id: &str) -> Option<&Party> {
101        self.parties.iter().find(|p| p.id == id)
102    }
103
104    /// Validate the roster for use in a session with the given threshold:
105    /// at least one party, no duplicate ids, and threshold in range.
106    pub fn validate(&self, threshold: u32) -> Result<()> {
107        ensure!(!self.parties.is_empty(), error::EmptyPartyListSnafu {});
108        ensure!(threshold >= 1, error::ThresholdTooSmallSnafu { threshold });
109        ensure!(
110            threshold as usize <= self.parties.len(),
111            error::ThresholdTooLargeSnafu {
112                threshold,
113                party_count: self.parties.len(),
114            }
115        );
116        for (i, p) in self.parties.iter().enumerate() {
117            for q in &self.parties[i + 1..] {
118                ensure!(p.id != q.id, error::DuplicatePartyIdSnafu { id: &p.id });
119            }
120        }
121        Ok(())
122    }
123}
124
125impl Default for PartyList {
126    fn default() -> Self {
127        PartyList::new()
128    }
129}
130
131impl fmt::Display for Party {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match &self.transport_endpoint {
134            Some(ep) => write!(f, "{}@{}", self.id, ep),
135            None => write!(f, "{}", self.id),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn party_inproc_has_no_endpoint() {
146        let p = Party::inproc("node-1");
147        assert_eq!(p.id, "node-1");
148        assert!(p.transport_endpoint.is_none());
149        assert_eq!(format!("{p}"), "node-1");
150    }
151
152    #[test]
153    fn party_display_with_endpoint() {
154        let p = Party::new("node-1", Some("quic://h:443".to_string()));
155        assert_eq!(format!("{p}"), "node-1@quic://h:443");
156    }
157
158    #[test]
159    fn party_list_get_out_of_range_errors() {
160        let list = PartyList::from_parties(vec![Party::inproc("a")]);
161        let err = list.get(5).unwrap_err();
162        assert!(
163            matches!(
164                err,
165                error::Error::PartyIndexOutOfRange {
166                    idx: 5,
167                    count: 1,
168                    ..
169                }
170            ),
171            "expected PartyIndexOutOfRange, got {err:?}"
172        );
173    }
174
175    #[test]
176    fn party_list_validate_rejects_empty() {
177        let list = PartyList::new();
178        assert!(list.validate(1).is_err());
179    }
180
181    #[test]
182    fn party_list_validate_rejects_zero_threshold() {
183        let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("b")]);
184        let err = list.validate(0).unwrap_err();
185        assert!(matches!(
186            err,
187            error::Error::ThresholdTooSmall { threshold: 0, .. }
188        ));
189    }
190
191    #[test]
192    fn party_list_validate_rejects_threshold_above_party_count() {
193        let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("b")]);
194        let err = list.validate(3).unwrap_err();
195        assert!(matches!(
196            err,
197            error::Error::ThresholdTooLarge {
198                threshold: 3,
199                party_count: 2,
200                ..
201            }
202        ));
203    }
204
205    #[test]
206    fn party_list_validate_rejects_duplicate_ids() {
207        let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("a")]);
208        let err = list.validate(1).unwrap_err();
209        assert!(matches!(err, error::Error::DuplicatePartyId { .. }));
210    }
211
212    #[test]
213    fn party_list_validate_accepts_valid_roster() {
214        let list = PartyList::from_parties(vec![
215            Party::inproc("a"),
216            Party::inproc("b"),
217            Party::inproc("c"),
218        ]);
219        list.validate(2).expect("valid roster should pass");
220    }
221
222    #[test]
223    fn party_list_find_by_id() {
224        let list = PartyList::from_parties(vec![Party::inproc("a"), Party::inproc("b")]);
225        assert_eq!(list.find("b").map(|p| p.id.as_str()), Some("b"));
226        assert!(list.find("z").is_none());
227    }
228}