confium_signerd/
daemon.rs1use crate::config::DaemonConfig;
4use confium_coordinator::coordinator::client::SignerClient;
5use confium_coordinator::coordinator::net::{ProtocolMessage, recv_message, send_message};
6use std::io;
7use std::path::Path;
8
9pub struct SignerDaemon {
12 config: DaemonConfig,
13}
14
15impl SignerDaemon {
16 pub fn new(config: DaemonConfig) -> Self {
18 Self { config }
19 }
20
21 pub fn run(&self) -> RunResult {
24 let mut attempts = 0u32;
25 loop {
26 tracing::info!(
27 addr = self
28 .config
29 .coordinator_url
30 .as_deref()
31 .unwrap_or(&self.config.coordinator_addr),
32 attempt = attempts,
33 "connecting to coordinator"
34 );
35 match self.connect_and_serve() {
36 Ok(()) => return RunResult::Disconnected,
37 Err(e) => {
38 tracing::warn!(error = %e, "connection lost");
39 attempts += 1;
40 let max = self.config.max_reconnect_attempts;
41 if max > 0 && attempts >= max {
42 tracing::error!(attempts, "max reconnect attempts reached, giving up");
43 return RunResult::MaxRetriesExhausted;
44 }
45 let backoff = self.config.reconnect_backoff_secs;
46 tracing::info!(backoff_secs = backoff, "sleeping before reconnect");
47 std::thread::sleep(std::time::Duration::from_secs(backoff));
48 }
49 }
50 }
51 }
52
53 fn connect_and_serve(&self) -> io::Result<()> {
54 let mut client = match &self.config.coordinator_url {
55 Some(url) => SignerClient::connect_url(url)?,
56 None => SignerClient::connect(&self.config.coordinator_addr)?,
57 };
58 client.register(&self.config.signer_id, &self.config.quorum_id)?;
59 tracing::info!(signer_id = %self.config.signer_id, "registered with coordinator");
60
61 let share_bytes = self.load_share()?;
62
63 loop {
64 let msg = client.recv()?;
65 match msg {
66 ProtocolMessage::SessionPending {
67 session_id,
68 message,
69 threshold: _,
70 } => {
71 tracing::info!(session = %session_id, "received signing request");
72 self.handle_signing_request(
73 client.stream_mut(),
74 &session_id,
75 &message,
76 &share_bytes,
77 )?;
78 }
79 ProtocolMessage::HealthCheck => {
80 send_message(
81 client.stream_mut(),
82 &ProtocolMessage::HealthStatus {
83 alive: true,
84 ready: true,
85 session_count: 0,
86 uptime_seconds: 0,
87 },
88 )?;
89 }
90 _ => {
91 tracing::debug!(msg = ?msg, "ignoring unexpected message");
92 }
93 }
94 }
95 }
96
97 fn handle_signing_request(
98 &self,
99 stream: &mut Box<dyn confium_coordinator::coordinator::net_server::SessionIo>,
100 session_id: &str,
101 _message: &[u8],
102 share_bytes: &[u8],
103 ) -> io::Result<()> {
104 let commitment = self.derive_commitment(share_bytes);
105 send_message(
106 stream,
107 &ProtocolMessage::Commitment {
108 session_id: session_id.into(),
109 signer_id: self.config.signer_id.clone(),
110 bytes: commitment,
111 signature: vec![0u8; 64],
112 },
113 )?;
114 let _ = recv_message(stream)?;
115
116 send_message(
117 stream,
118 &ProtocolMessage::Share {
119 session_id: session_id.into(),
120 signer_id: self.config.signer_id.clone(),
121 bytes: share_bytes.to_vec(),
122 signature: vec![0u8; 64],
123 },
124 )?;
125
126 match recv_message(stream) {
127 Ok(ProtocolMessage::Signature { bytes, .. }) => {
128 tracing::info!(
129 session = session_id,
130 sig_len = bytes.len(),
131 "signature aggregated"
132 );
133 }
134 Ok(ProtocolMessage::Ack { .. }) => {
135 tracing::info!(
136 session = session_id,
137 "share submitted, waiting for more signers"
138 );
139 }
140 Ok(ProtocolMessage::Error { message }) => {
141 tracing::error!(session = session_id, error = %message, "signing failed");
142 }
143 _ => {}
144 }
145 Ok(())
146 }
147
148 fn derive_commitment(&self, share_bytes: &[u8]) -> Vec<u8> {
149 use sha2::{Digest, Sha256};
150 let mut hasher = Sha256::new();
151 hasher.update(share_bytes);
152 hasher.update(self.config.signer_id.as_bytes());
153 hasher.update([0u8; 8]);
154 hasher.finalize().to_vec()
155 }
156
157 fn load_share(&self) -> io::Result<Vec<u8>> {
158 let path = Path::new(&self.config.share_file);
159 std::fs::read(path).map_err(|e| {
160 io::Error::new(
161 io::ErrorKind::NotFound,
162 format!("share file {}: {e}", path.display()),
163 )
164 })
165 }
166}
167
168#[derive(Debug)]
170pub enum RunResult {
171 Disconnected,
173 MaxRetriesExhausted,
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::io::Write;
181
182 fn make_config() -> DaemonConfig {
183 DaemonConfig {
184 coordinator_url: None,
185 coordinator_addr: "127.0.0.1:0".into(),
186 signer_id: "test-signer".into(),
187 quorum_id: "test-quorum".into(),
188 share_file: "/dev/null".into(),
189 scheme: "CMP20".into(),
190 reconnect_backoff_secs: 1,
191 max_reconnect_attempts: 1,
192 }
193 }
194
195 #[test]
196 fn daemon_constructs_from_config() {
197 let config = make_config();
198 let daemon = SignerDaemon::new(config);
199 assert_eq!(daemon.config.signer_id, "test-signer");
200 }
201
202 #[test]
203 fn derive_commitment_is_deterministic() {
204 let config = make_config();
205 let daemon = SignerDaemon::new(config);
206 let share = vec![0xAA; 32];
207 let c1 = daemon.derive_commitment(&share);
208 let c2 = daemon.derive_commitment(&share);
209 assert_eq!(c1, c2);
210 assert_eq!(c1.len(), 32);
211 }
212
213 #[test]
214 fn derive_commitment_differs_for_different_shares() {
215 let config = make_config();
216 let daemon = SignerDaemon::new(config);
217 let c1 = daemon.derive_commitment(&[0xAA; 32]);
218 let c2 = daemon.derive_commitment(&[0xBB; 32]);
219 assert_ne!(c1, c2);
220 }
221
222 #[test]
223 fn load_share_reads_file() {
224 let mut tmp = tempfile::NamedTempFile::new().unwrap();
225 tmp.write_all(&[0x42; 64]).unwrap();
226 let mut config = make_config();
227 config.share_file = tmp.path().to_string_lossy().to_string();
228 let daemon = SignerDaemon::new(config);
229 let share = daemon.load_share().unwrap();
230 assert_eq!(share, vec![0x42; 64]);
231 }
232
233 #[test]
234 fn load_share_missing_file_errors() {
235 let mut config = make_config();
236 config.share_file = "/nonexistent/path/share.json".into();
237 let daemon = SignerDaemon::new(config);
238 assert!(daemon.load_share().is_err());
239 }
240}