confium_signerd/
config.rs1use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DaemonConfig {
11 pub coordinator_addr: String,
14 #[serde(default)]
18 pub coordinator_url: Option<String>,
19 pub signer_id: String,
21 pub quorum_id: String,
23 pub share_file: String,
25 pub scheme: String,
27 #[serde(default = "default_backoff")]
29 pub reconnect_backoff_secs: u64,
30 #[serde(default = "default_max_retries")]
32 pub max_reconnect_attempts: u32,
33}
34
35fn default_backoff() -> u64 {
36 5
37}
38
39fn default_max_retries() -> u32 {
40 0
41}
42
43impl DaemonConfig {
44 pub fn load(path: &Path) -> Result<Self, ConfigError> {
46 let contents = std::fs::read_to_string(path)
47 .map_err(|e| ConfigError::Read(path.display().to_string(), e.to_string()))?;
48 let config: Self = toml::from_str(&contents)
49 .map_err(|e| ConfigError::Parse(path.display().to_string(), e.to_string()))?;
50 config.validate()?;
51 Ok(config)
52 }
53
54 fn validate(&self) -> Result<(), ConfigError> {
55 if self.signer_id.is_empty() {
56 return Err(ConfigError::Invalid("signer_id must not be empty".into()));
57 }
58 if self.quorum_id.is_empty() {
59 return Err(ConfigError::Invalid("quorum_id must not be empty".into()));
60 }
61 if self.coordinator_url.is_none() && self.coordinator_addr.is_empty() {
62 return Err(ConfigError::Invalid(
63 "coordinator_addr must not be empty".into(),
64 ));
65 }
66 Ok(())
67 }
68}
69
70#[derive(Debug, thiserror::Error)]
72pub enum ConfigError {
73 #[error("failed to read {0}: {1}")]
74 Read(String, String),
75 #[error("failed to parse {0}: {1}")]
76 Parse(String, String),
77 #[error("invalid configuration: {0}")]
78 Invalid(String),
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::io::Write;
85
86 #[test]
87 fn valid_config_parses() {
88 let toml_str = r#"
89coordinator_addr = "127.0.0.1:18432"
90signer_id = "director-1"
91quorum_id = "biml-root"
92share_file = "/etc/confium/director-1.json"
93scheme = "CMP20"
94"#;
95 let mut tmp = tempfile::NamedTempFile::new().unwrap();
96 tmp.write_all(toml_str.as_bytes()).unwrap();
97 let config = DaemonConfig::load(tmp.path()).unwrap();
98 assert_eq!(config.signer_id, "director-1");
99 assert_eq!(config.quorum_id, "biml-root");
100 assert_eq!(config.scheme, "CMP20");
101 assert_eq!(config.reconnect_backoff_secs, 5);
102 }
103
104 #[test]
105 fn empty_signer_id_rejected() {
106 let toml_str = r#"
107coordinator_addr = "127.0.0.1:18432"
108signer_id = ""
109quorum_id = "q"
110share_file = "/tmp/share.json"
111scheme = "CMP20"
112"#;
113 let mut tmp = tempfile::NamedTempFile::new().unwrap();
114 tmp.write_all(toml_str.as_bytes()).unwrap();
115 assert!(DaemonConfig::load(tmp.path()).is_err());
116 }
117
118 #[test]
119 fn custom_backoff_parses() {
120 let toml_str = r#"
121coordinator_addr = "127.0.0.1:18432"
122signer_id = "s1"
123quorum_id = "q"
124share_file = "/tmp/share.json"
125scheme = "CMP20"
126reconnect_backoff_secs = 30
127max_reconnect_attempts = 10
128"#;
129 let mut tmp = tempfile::NamedTempFile::new().unwrap();
130 tmp.write_all(toml_str.as_bytes()).unwrap();
131 let config = DaemonConfig::load(tmp.path()).unwrap();
132 assert_eq!(config.reconnect_backoff_secs, 30);
133 assert_eq!(config.max_reconnect_attempts, 10);
134 }
135}