qudag_protocol/
config.rs

1//! Protocol configuration implementation.
2
3use serde::{Deserialize, Serialize};
4use std::env;
5use std::fs;
6use std::path::PathBuf;
7use std::time::Duration;
8use thiserror::Error;
9
10/// Configuration-related errors
11#[derive(Debug, Error)]
12pub enum ConfigError {
13    /// Invalid configuration value
14    #[error("Invalid configuration: {0}")]
15    InvalidValue(String),
16
17    /// Configuration file not found
18    #[error("Configuration file not found: {0}")]
19    FileNotFound(String),
20
21    /// Configuration parsing error
22    #[error("Parse error: {0}")]
23    ParseError(String),
24
25    /// Environment variable error
26    #[error("Environment variable error: {0}")]
27    EnvError(String),
28
29    /// IO error
30    #[error("IO error: {0}")]
31    IoError(#[from] std::io::Error),
32
33    /// Serialization error
34    #[error("Serialization error: {0}")]
35    SerializationError(#[from] serde_json::Error),
36}
37
38/// Protocol configuration
39#[derive(Debug, Clone, Serialize, Deserialize, Default)]
40pub struct Config {
41    /// Node configuration
42    pub node: NodeConfig,
43
44    /// Network configuration
45    pub network: NetworkConfig,
46
47    /// Consensus configuration
48    pub consensus: ConsensusConfig,
49}
50
51/// Node-specific configuration
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct NodeConfig {
54    /// Node ID
55    pub node_id: String,
56
57    /// Data directory
58    pub data_dir: PathBuf,
59
60    /// Log level
61    pub log_level: String,
62}
63
64/// Network configuration
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct NetworkConfig {
67    /// Listen port
68    pub port: u16,
69
70    /// Maximum number of peers
71    pub max_peers: usize,
72
73    /// Connection timeout
74    pub connect_timeout: Duration,
75}
76
77/// Consensus configuration
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ConsensusConfig {
80    /// Finality threshold
81    pub finality_threshold: f64,
82
83    /// Round timeout
84    pub round_timeout: Duration,
85
86    /// Maximum rounds
87    pub max_rounds: usize,
88}
89
90impl Default for NodeConfig {
91    fn default() -> Self {
92        Self {
93            node_id: "node-0".to_string(),
94            data_dir: PathBuf::from("./data"),
95            log_level: "info".to_string(),
96        }
97    }
98}
99
100impl Default for NetworkConfig {
101    fn default() -> Self {
102        Self {
103            port: 8080,
104            max_peers: 50,
105            connect_timeout: Duration::from_secs(30),
106        }
107    }
108}
109
110impl Default for ConsensusConfig {
111    fn default() -> Self {
112        Self {
113            finality_threshold: 0.67,
114            round_timeout: Duration::from_secs(10),
115            max_rounds: 100,
116        }
117    }
118}
119
120impl Config {
121    /// Load configuration from file with optional environment variable overrides
122    pub fn load_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
123        let content = fs::read_to_string(&path)
124            .map_err(|_| ConfigError::FileNotFound(path.as_ref().display().to_string()))?;
125
126        let mut config: Config =
127            serde_json::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
128
129        // Apply environment variable overrides
130        config.apply_env_overrides()?;
131
132        // Validate configuration
133        config.validate()?;
134
135        Ok(config)
136    }
137
138    /// Load configuration from TOML file
139    pub fn load_from_toml<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
140        let content = fs::read_to_string(&path)
141            .map_err(|_| ConfigError::FileNotFound(path.as_ref().display().to_string()))?;
142
143        let mut config: Config =
144            toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
145
146        // Apply environment variable overrides
147        config.apply_env_overrides()?;
148
149        // Validate configuration
150        config.validate()?;
151
152        Ok(config)
153    }
154
155    /// Apply environment variable overrides
156    pub fn apply_env_overrides(&mut self) -> Result<(), ConfigError> {
157        // Node configuration overrides
158        if let Ok(node_id) = env::var("QUDAG_NODE_ID") {
159            self.node.node_id = node_id;
160        }
161
162        if let Ok(data_dir) = env::var("QUDAG_DATA_DIR") {
163            self.node.data_dir = PathBuf::from(data_dir);
164        }
165
166        if let Ok(log_level) = env::var("QUDAG_LOG_LEVEL") {
167            self.node.log_level = log_level;
168        }
169
170        // Network configuration overrides
171        if let Ok(port) = env::var("QUDAG_PORT") {
172            self.network.port = port
173                .parse()
174                .map_err(|e| ConfigError::EnvError(format!("Invalid port: {}", e)))?;
175        }
176
177        if let Ok(max_peers) = env::var("QUDAG_MAX_PEERS") {
178            self.network.max_peers = max_peers
179                .parse()
180                .map_err(|e| ConfigError::EnvError(format!("Invalid max_peers: {}", e)))?;
181        }
182
183        if let Ok(timeout) = env::var("QUDAG_CONNECT_TIMEOUT") {
184            let timeout_secs: u64 = timeout
185                .parse()
186                .map_err(|e| ConfigError::EnvError(format!("Invalid connect_timeout: {}", e)))?;
187            self.network.connect_timeout = Duration::from_secs(timeout_secs);
188        }
189
190        // Consensus configuration overrides
191        if let Ok(threshold) = env::var("QUDAG_FINALITY_THRESHOLD") {
192            self.consensus.finality_threshold = threshold
193                .parse()
194                .map_err(|e| ConfigError::EnvError(format!("Invalid finality_threshold: {}", e)))?;
195        }
196
197        if let Ok(timeout) = env::var("QUDAG_ROUND_TIMEOUT") {
198            let timeout_secs: u64 = timeout
199                .parse()
200                .map_err(|e| ConfigError::EnvError(format!("Invalid round_timeout: {}", e)))?;
201            self.consensus.round_timeout = Duration::from_secs(timeout_secs);
202        }
203
204        if let Ok(max_rounds) = env::var("QUDAG_MAX_ROUNDS") {
205            self.consensus.max_rounds = max_rounds
206                .parse()
207                .map_err(|e| ConfigError::EnvError(format!("Invalid max_rounds: {}", e)))?;
208        }
209
210        Ok(())
211    }
212
213    /// Validate configuration values
214    pub fn validate(&self) -> Result<(), ConfigError> {
215        // Validate node configuration
216        if self.node.node_id.is_empty() {
217            return Err(ConfigError::InvalidValue(
218                "node_id cannot be empty".to_string(),
219            ));
220        }
221
222        if self.node.log_level.is_empty() {
223            return Err(ConfigError::InvalidValue(
224                "log_level cannot be empty".to_string(),
225            ));
226        }
227
228        // Validate allowed log levels
229        match self.node.log_level.as_str() {
230            "trace" | "debug" | "info" | "warn" | "error" => {}
231            _ => {
232                return Err(ConfigError::InvalidValue(format!(
233                    "Invalid log_level: {}",
234                    self.node.log_level
235                )))
236            }
237        }
238
239        // Validate network configuration
240        if self.network.port == 0 {
241            return Err(ConfigError::InvalidValue("port cannot be 0".to_string()));
242        }
243
244        if self.network.max_peers == 0 {
245            return Err(ConfigError::InvalidValue(
246                "max_peers must be > 0".to_string(),
247            ));
248        }
249
250        if self.network.max_peers > 10000 {
251            return Err(ConfigError::InvalidValue(
252                "max_peers must be <= 10000".to_string(),
253            ));
254        }
255
256        if self.network.connect_timeout.is_zero() {
257            return Err(ConfigError::InvalidValue(
258                "connect_timeout must be > 0".to_string(),
259            ));
260        }
261
262        if self.network.connect_timeout > Duration::from_secs(300) {
263            return Err(ConfigError::InvalidValue(
264                "connect_timeout must be <= 300s".to_string(),
265            ));
266        }
267
268        // Validate consensus configuration
269        if self.consensus.finality_threshold <= 0.0 || self.consensus.finality_threshold > 1.0 {
270            return Err(ConfigError::InvalidValue(
271                "finality_threshold must be between 0.0 and 1.0".to_string(),
272            ));
273        }
274
275        if self.consensus.round_timeout.is_zero() {
276            return Err(ConfigError::InvalidValue(
277                "round_timeout must be > 0".to_string(),
278            ));
279        }
280
281        if self.consensus.max_rounds == 0 {
282            return Err(ConfigError::InvalidValue(
283                "max_rounds must be > 0".to_string(),
284            ));
285        }
286
287        if self.consensus.max_rounds > 1000 {
288            return Err(ConfigError::InvalidValue(
289                "max_rounds must be <= 1000".to_string(),
290            ));
291        }
292
293        Ok(())
294    }
295
296    /// Save configuration to file
297    pub fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigError> {
298        let content = serde_json::to_string_pretty(self)?;
299        fs::write(path, content)?;
300        Ok(())
301    }
302
303    /// Save configuration to TOML file
304    pub fn save_to_toml<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigError> {
305        let content = toml::to_string_pretty(self).map_err(|e| {
306            ConfigError::SerializationError(serde_json::Error::io(std::io::Error::new(
307                std::io::ErrorKind::InvalidData,
308                e,
309            )))
310        })?;
311        fs::write(path, content)?;
312        Ok(())
313    }
314}