Skip to main content

confium_tc/coordinator/
net.rs

1//! TCP network protocol for coordinator ↔ signer communication.
2//!
3//! Wire format: 4-byte big-endian length prefix + JSON payload.
4//!
5//! Messages flow in both directions:
6//! - Signer → Coordinator: Register, Commitment, Share
7//! - Coordinator → Signer: Registered, SessionPending, CommitmentsReady, Signature
8//! - Client → Coordinator: CreateSession, GetStatus
9//! - Coordinator → Client: SessionCreated, Signature, Error
10
11use crate::coordinator::session::SignerId;
12use serde::{Deserialize, Serialize};
13use std::io::{self, Read, Write};
14use std::net::TcpStream;
15
16/// Protocol message exchanged over TCP between coordinator, signers, and clients.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "type")]
19pub enum ProtocolMessage {
20    /// Signer registers with coordinator.
21    Register {
22        /// Signer identity.
23        signer_id: SignerId,
24        /// Quorum this signer belongs to.
25        quorum_id: String,
26    },
27    /// Coordinator acknowledges registration.
28    Registered {
29        /// Signer identity.
30        signer_id: SignerId,
31    },
32    /// Client requests session creation.
33    CreateSession {
34        /// Quorum ID.
35        quorum_id: String,
36        /// Signing scheme.
37        scheme: String,
38        /// Message to sign.
39        message: Vec<u8>,
40        /// Threshold T.
41        threshold: u32,
42        /// Total parties N.
43        num_parties: u32,
44    },
45    /// Coordinator confirms session created.
46    SessionCreated {
47        /// Session ID.
48        session_id: String,
49    },
50    /// Coordinator notifies signers of pending session.
51    SessionPending {
52        /// Session ID.
53        session_id: String,
54        /// Message to sign.
55        message: Vec<u8>,
56        /// Threshold.
57        threshold: u32,
58    },
59    /// Signer submits commitment.
60    Commitment {
61        /// Session ID.
62        session_id: String,
63        /// Signer identity.
64        signer_id: SignerId,
65        /// Commitment bytes.
66        bytes: Vec<u8>,
67        /// Identity signature.
68        signature: Vec<u8>,
69    },
70    /// Coordinator notifies that commitments are collected.
71    CommitmentsReady {
72        /// Session ID.
73        session_id: String,
74    },
75    /// Signer submits share.
76    Share {
77        /// Session ID.
78        session_id: String,
79        /// Signer identity.
80        signer_id: SignerId,
81        /// Share bytes.
82        bytes: Vec<u8>,
83        /// Identity signature.
84        signature: Vec<u8>,
85    },
86    /// Coordinator returns aggregated signature.
87    Signature {
88        /// Session ID.
89        session_id: String,
90        /// Signature bytes.
91        bytes: Vec<u8>,
92        /// Algorithm.
93        algorithm: String,
94        /// Contributing signers.
95        contributing_signers: Vec<SignerId>,
96    },
97    /// Acknowledgement (commitment or share accepted, no further action needed).
98    Ack {
99        /// Session ID being acknowledged.
100        session_id: String,
101    },
102    /// Error response.
103    Error {
104        /// Error message.
105        message: String,
106    },
107    /// Status query.
108    GetStatus {
109        /// Session ID (optional).
110        session_id: Option<String>,
111    },
112    /// Status response.
113    Status {
114        /// Session ID.
115        session_id: String,
116        /// Current state.
117        state: String,
118    },
119    /// Health check (used by signerd to verify coordinator is alive).
120    HealthCheck,
121    /// Health check response.
122    HealthStatus {
123        /// Server is alive.
124        alive: bool,
125        /// Coordinator is ready.
126        ready: bool,
127        /// Active session count.
128        session_count: usize,
129        /// Server uptime in seconds.
130        uptime_seconds: u64,
131    },
132}
133
134/// Send a protocol message over a TCP stream.
135pub fn send_message(stream: &mut TcpStream, msg: &ProtocolMessage) -> io::Result<()> {
136    let json = serde_json::to_vec(msg)?;
137    let len = json.len() as u32;
138    stream.write_all(&len.to_be_bytes())?;
139    stream.write_all(&json)?;
140    stream.flush()?;
141    Ok(())
142}
143
144/// Receive a protocol message from a TCP stream.
145pub fn recv_message(stream: &mut TcpStream) -> io::Result<ProtocolMessage> {
146    let mut len_buf = [0u8; 4];
147    stream.read_exact(&mut len_buf)?;
148    let len = u32::from_be_bytes(len_buf) as usize;
149
150    if len > 16 * 1024 * 1024 {
151        return Err(io::Error::new(
152            io::ErrorKind::InvalidData,
153            format!("message too large: {} bytes", len),
154        ));
155    }
156
157    let mut json_buf = vec![0u8; len];
158    stream.read_exact(&mut json_buf)?;
159
160    let msg: ProtocolMessage = serde_json::from_slice(&json_buf)?;
161    Ok(msg)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn protocol_message_serializes() {
170        let msg = ProtocolMessage::Register {
171            signer_id: "alice".into(),
172            quorum_id: "test".into(),
173        };
174        let json = serde_json::to_vec(&msg).unwrap();
175        assert!(json.len() > 10);
176        let recovered: ProtocolMessage = serde_json::from_slice(&json).unwrap();
177        match recovered {
178            ProtocolMessage::Register {
179                signer_id,
180                quorum_id,
181            } => {
182                assert_eq!(signer_id, "alice");
183                assert_eq!(quorum_id, "test");
184            }
185            _ => panic!("wrong variant"),
186        }
187    }
188
189    #[test]
190    fn all_variants_round_trip() {
191        let messages = vec![
192            ProtocolMessage::Register {
193                signer_id: "a".into(),
194                quorum_id: "q".into(),
195            },
196            ProtocolMessage::Registered {
197                signer_id: "a".into(),
198            },
199            ProtocolMessage::CreateSession {
200                quorum_id: "q".into(),
201                scheme: "FROST-P256".into(),
202                message: vec![1, 2, 3],
203                threshold: 3,
204                num_parties: 5,
205            },
206            ProtocolMessage::SessionCreated {
207                session_id: "s1".into(),
208            },
209            ProtocolMessage::SessionPending {
210                session_id: "s1".into(),
211                message: vec![1, 2, 3],
212                threshold: 3,
213            },
214            ProtocolMessage::Commitment {
215                session_id: "s1".into(),
216                signer_id: "a".into(),
217                bytes: vec![4, 5],
218                signature: vec![6, 7],
219            },
220            ProtocolMessage::Signature {
221                session_id: "s1".into(),
222                bytes: vec![8, 9],
223                algorithm: "FROST-P256".into(),
224                contributing_signers: vec!["a".into(), "b".into()],
225            },
226            ProtocolMessage::Error {
227                message: "test".into(),
228            },
229        ];
230        for msg in &messages {
231            let json = serde_json::to_vec(msg).unwrap();
232            let recovered: ProtocolMessage = serde_json::from_slice(&json).unwrap();
233            let json2 = serde_json::to_vec(&recovered).unwrap();
234            assert_eq!(json, json2, "round-trip must preserve bytes");
235        }
236    }
237}