Skip to main content

confium_tc_core/
message.rs

1//! Round message exchanged between parties.
2//!
3//! A [`Message`] is the unit of communication in a threshold session.
4//! Each round, a party emits zero or more [`Message`]s (either
5//! broadcast — `to_party_id == None` — or directed to a specific peer)
6//! and consumes the [`Message`]s it received from the previous round.
7//!
8//! The framework is transport-agnostic: it produces and consumes
9//! [`Message`]s. Wiring [`Message`]s to a Network transport is a
10//! separate concern handled by the session driver (see
11//! `TODO.roadmap/05-networking-primitives.md`).
12
13use std::fmt;
14
15/// A single inter-party message belonging to one round of a session.
16///
17/// `from_party_id` and `to_party_id` are the canonical ASCII party ids
18/// from [`crate::party::Party`]. `to_party_id == None` means broadcast
19/// — every other party should process this message. `round` is the
20/// 1-indexed round number the message belongs to; `payload` is the
21/// scheme-specific wire bytes (commitments, signature shares, etc.).
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Message {
24    pub from_party_id: String,
25    pub to_party_id: Option<String>,
26    pub round: u8,
27    pub payload: Vec<u8>,
28}
29
30impl Message {
31    /// Build a directed (point-to-point) message.
32    pub fn directed(
33        from: impl Into<String>,
34        to: impl Into<String>,
35        round: u8,
36        payload: impl Into<Vec<u8>>,
37    ) -> Self {
38        Message {
39            from_party_id: from.into(),
40            to_party_id: Some(to.into()),
41            round,
42            payload: payload.into(),
43        }
44    }
45
46    /// Build a broadcast message — addressed to every other party.
47    pub fn broadcast(from: impl Into<String>, round: u8, payload: impl Into<Vec<u8>>) -> Self {
48        Message {
49            from_party_id: from.into(),
50            to_party_id: None,
51            round,
52            payload: payload.into(),
53        }
54    }
55
56    /// True when this message is addressed to every party.
57    pub fn is_broadcast(&self) -> bool {
58        self.to_party_id.is_none()
59    }
60
61    /// True when this message is directed at exactly one party.
62    pub fn is_directed(&self) -> bool {
63        self.to_party_id.is_some()
64    }
65
66    /// True when the message is intended for `party_id` — either a
67    /// broadcast or a directed message whose `to_party_id` matches.
68    pub fn is_for(&self, party_id: &str) -> bool {
69        match &self.to_party_id {
70            None => true,
71            Some(to) => to == party_id,
72        }
73    }
74}
75
76impl fmt::Display for Message {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        let to = self.to_party_id.as_deref().unwrap_or("*");
79        write!(
80            f,
81            "round {} {} -> {} ({} bytes)",
82            self.round,
83            self.from_party_id,
84            to,
85            self.payload.len()
86        )
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn broadcast_message_has_null_recipient() {
96        let m = Message::broadcast("node-1", 1, [0xAA, 0xBB]);
97        assert!(m.is_broadcast());
98        assert!(!m.is_directed());
99        assert!(m.to_party_id.is_none());
100    }
101
102    #[test]
103    fn directed_message_has_recipient() {
104        let m = Message::directed("node-1", "node-2", 2, [0x01]);
105        assert!(!m.is_broadcast());
106        assert!(m.is_directed());
107        assert_eq!(m.to_party_id.as_deref(), Some("node-2"));
108    }
109
110    #[test]
111    fn is_for_matches_directed_recipient() {
112        let m = Message::directed("a", "b", 1, []);
113        assert!(m.is_for("b"));
114        assert!(!m.is_for("a"));
115        assert!(!m.is_for("c"));
116    }
117
118    #[test]
119    fn is_for_matches_everyone_when_broadcast() {
120        let m = Message::broadcast("a", 1, []);
121        assert!(m.is_for("b"));
122        assert!(m.is_for("c"));
123        // Broadcast is also "for" the sender per this predicate — the
124        // driver layer is responsible for not echoing to self.
125        assert!(m.is_for("a"));
126    }
127
128    #[test]
129    fn display_format_includes_round_and_parties() {
130        let m = Message::directed("a", "b", 3, [0u8; 4]);
131        assert_eq!(format!("{m}"), "round 3 a -> b (4 bytes)");
132    }
133
134    #[test]
135    fn display_format_uses_star_for_broadcast() {
136        let m = Message::broadcast("a", 1, [0u8; 2]);
137        assert_eq!(format!("{m}"), "round 1 a -> * (2 bytes)");
138    }
139
140    #[test]
141    fn equality_compares_all_fields() {
142        let a = Message::directed("p1", "p2", 1, vec![1, 2]);
143        let b = Message::directed("p1", "p2", 1, vec![1, 2]);
144        assert_eq!(a, b);
145
146        let c = Message::directed("p1", "p2", 2, vec![1, 2]);
147        assert_ne!(a, c);
148
149        let d = Message::directed("p1", "p3", 1, vec![1, 2]);
150        assert_ne!(a, d);
151    }
152}