guts_node/
config.rs

1//! # Node Configuration
2//!
3//! Production-grade configuration management with:
4//!
5//! - Environment variable support (12-factor app)
6//! - Configuration file loading (YAML/TOML)
7//! - Comprehensive validation
8//! - Sensible defaults
9//!
10//! ## Environment Variables
11//!
12//! | Variable | Description | Default |
13//! |----------|-------------|---------|
14//! | `GUTS_API_ADDR` | HTTP API address | `127.0.0.1:8080` |
15//! | `GUTS_P2P_ADDR` | P2P listen address | `0.0.0.0:9000` |
16//! | `GUTS_METRICS_ADDR` | Metrics endpoint | `0.0.0.0:9090` |
17//! | `GUTS_LOG_LEVEL` | Log level | `info` |
18//! | `GUTS_LOG_FORMAT` | Log format (json/pretty) | `json` |
19//! | `GUTS_PRIVATE_KEY` | Ed25519 private key (hex) | *required for P2P* |
20//! | `GUTS_DATA_DIR` | Data directory | `./data` |
21//!
22//! ## Usage
23//!
24//! ```rust,ignore
25//! use guts_node::config::NodeConfig;
26//!
27//! let config = NodeConfig::from_env().expect("Invalid configuration");
28//! config.validate_config().expect("Configuration validation failed");
29//! ```
30
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::net::SocketAddr;
34use std::path::PathBuf;
35use std::time::Duration;
36use validator::Validate;
37
38/// Configuration validation errors.
39#[derive(Debug, thiserror::Error)]
40pub enum ConfigError {
41    /// Invalid configuration value.
42    #[error("Invalid configuration: {0}")]
43    Invalid(String),
44
45    /// Missing required configuration.
46    #[error("Missing required configuration: {0}")]
47    Missing(String),
48
49    /// Environment variable parsing error.
50    #[error("Failed to parse environment variable {key}: {message}")]
51    EnvParse { key: String, message: String },
52
53    /// File loading error.
54    #[error("Failed to load configuration file: {0}")]
55    FileLoad(String),
56
57    /// Validation error.
58    #[error("Validation failed: {0}")]
59    Validation(String),
60}
61
62/// Main node configuration.
63#[derive(Debug, Clone, Default, Deserialize, Serialize, Validate)]
64pub struct NodeConfig {
65    /// HTTP API configuration.
66    #[validate(nested)]
67    #[serde(default)]
68    pub api: ApiConfig,
69
70    /// P2P network configuration.
71    #[validate(nested)]
72    #[serde(default)]
73    pub p2p: P2pConfig,
74
75    /// Consensus configuration.
76    #[validate(nested)]
77    #[serde(default)]
78    pub consensus: ConsensusConfig,
79
80    /// Metrics configuration.
81    #[validate(nested)]
82    #[serde(default)]
83    pub metrics: MetricsConfig,
84
85    /// Logging configuration.
86    #[validate(nested)]
87    #[serde(default)]
88    pub logging: LoggingConfig,
89
90    /// Storage configuration.
91    #[validate(nested)]
92    #[serde(default)]
93    pub storage: StorageConfig,
94
95    /// Resilience configuration.
96    #[validate(nested)]
97    #[serde(default)]
98    pub resilience: ResilienceConfig,
99}
100
101impl NodeConfig {
102    /// Load configuration from environment variables.
103    pub fn from_env() -> Result<Self, ConfigError> {
104        let mut config = Self::default();
105
106        // API configuration
107        if let Ok(addr) = std::env::var("GUTS_API_ADDR") {
108            config.api.addr = addr.parse().map_err(|_| ConfigError::EnvParse {
109                key: "GUTS_API_ADDR".to_string(),
110                message: "Invalid socket address".to_string(),
111            })?;
112        }
113
114        if let Ok(timeout) = std::env::var("GUTS_REQUEST_TIMEOUT") {
115            config.api.request_timeout_secs =
116                timeout.parse().map_err(|_| ConfigError::EnvParse {
117                    key: "GUTS_REQUEST_TIMEOUT".to_string(),
118                    message: "Invalid timeout value".to_string(),
119                })?;
120        }
121
122        // P2P configuration
123        if let Ok(addr) = std::env::var("GUTS_P2P_ADDR") {
124            config.p2p.addr = addr.parse().map_err(|_| ConfigError::EnvParse {
125                key: "GUTS_P2P_ADDR".to_string(),
126                message: "Invalid socket address".to_string(),
127            })?;
128        }
129
130        if let Ok(key) = std::env::var("GUTS_PRIVATE_KEY") {
131            config.p2p.private_key = Some(key);
132        }
133
134        if let Ok(enabled) = std::env::var("GUTS_P2P_ENABLED") {
135            config.p2p.enabled = enabled.parse().unwrap_or(true);
136        }
137
138        // Metrics configuration
139        if let Ok(addr) = std::env::var("GUTS_METRICS_ADDR") {
140            config.metrics.addr = addr.parse().map_err(|_| ConfigError::EnvParse {
141                key: "GUTS_METRICS_ADDR".to_string(),
142                message: "Invalid socket address".to_string(),
143            })?;
144        }
145
146        if let Ok(enabled) = std::env::var("GUTS_METRICS_ENABLED") {
147            config.metrics.enabled = enabled.parse().unwrap_or(true);
148        }
149
150        // Logging configuration
151        if let Ok(level) = std::env::var("GUTS_LOG_LEVEL") {
152            config.logging.level = level;
153        }
154
155        if let Ok(format) = std::env::var("GUTS_LOG_FORMAT") {
156            config.logging.format = format;
157        }
158
159        // Storage configuration
160        if let Ok(dir) = std::env::var("GUTS_DATA_DIR") {
161            config.storage.data_dir = PathBuf::from(dir);
162        }
163
164        // Consensus configuration
165        if let Ok(enabled) = std::env::var("GUTS_CONSENSUS_ENABLED") {
166            config.consensus.enabled = enabled.parse().unwrap_or(false);
167        }
168
169        if let Ok(use_simplex) = std::env::var("GUTS_CONSENSUS_USE_SIMPLEX_BFT") {
170            config.consensus.use_simplex_bft = use_simplex.parse().unwrap_or(false);
171        }
172
173        if let Ok(block_time) = std::env::var("GUTS_CONSENSUS_BLOCK_TIME_MS") {
174            config.consensus.block_time_ms =
175                block_time.parse().map_err(|_| ConfigError::EnvParse {
176                    key: "GUTS_CONSENSUS_BLOCK_TIME_MS".to_string(),
177                    message: "Invalid block time value".to_string(),
178                })?;
179        }
180
181        if let Ok(max_txs) = std::env::var("GUTS_CONSENSUS_MAX_TXS_PER_BLOCK") {
182            config.consensus.max_txs_per_block =
183                max_txs.parse().map_err(|_| ConfigError::EnvParse {
184                    key: "GUTS_CONSENSUS_MAX_TXS_PER_BLOCK".to_string(),
185                    message: "Invalid max transactions value".to_string(),
186                })?;
187        }
188
189        if let Ok(mempool_max) = std::env::var("GUTS_CONSENSUS_MEMPOOL_MAX_TXS") {
190            config.consensus.mempool_max_txs =
191                mempool_max.parse().map_err(|_| ConfigError::EnvParse {
192                    key: "GUTS_CONSENSUS_MEMPOOL_MAX_TXS".to_string(),
193                    message: "Invalid mempool max transactions value".to_string(),
194                })?;
195        }
196
197        if let Ok(mempool_ttl) = std::env::var("GUTS_CONSENSUS_MEMPOOL_TTL_SECS") {
198            config.consensus.mempool_ttl_secs =
199                mempool_ttl.parse().map_err(|_| ConfigError::EnvParse {
200                    key: "GUTS_CONSENSUS_MEMPOOL_TTL_SECS".to_string(),
201                    message: "Invalid mempool TTL value".to_string(),
202                })?;
203        }
204
205        if let Ok(genesis_file) = std::env::var("GUTS_CONSENSUS_GENESIS_FILE") {
206            config.consensus.genesis_file = Some(PathBuf::from(genesis_file));
207        }
208
209        Ok(config)
210    }
211
212    /// Load configuration from a YAML file.
213    pub fn from_file(path: &std::path::Path) -> Result<Self, ConfigError> {
214        let content =
215            std::fs::read_to_string(path).map_err(|e| ConfigError::FileLoad(e.to_string()))?;
216
217        serde_yaml::from_str(&content).map_err(|e| ConfigError::FileLoad(e.to_string()))
218    }
219
220    /// Merge configuration from environment variables.
221    pub fn merge_env(&mut self) -> Result<(), ConfigError> {
222        let env_config = Self::from_env()?;
223
224        // Only override if explicitly set in environment
225        if std::env::var("GUTS_API_ADDR").is_ok() {
226            self.api.addr = env_config.api.addr;
227        }
228        if std::env::var("GUTS_P2P_ADDR").is_ok() {
229            self.p2p.addr = env_config.p2p.addr;
230        }
231        if std::env::var("GUTS_PRIVATE_KEY").is_ok() {
232            self.p2p.private_key = env_config.p2p.private_key;
233        }
234        if std::env::var("GUTS_METRICS_ADDR").is_ok() {
235            self.metrics.addr = env_config.metrics.addr;
236        }
237        if std::env::var("GUTS_LOG_LEVEL").is_ok() {
238            self.logging.level = env_config.logging.level;
239        }
240        if std::env::var("GUTS_LOG_FORMAT").is_ok() {
241            self.logging.format = env_config.logging.format;
242        }
243        if std::env::var("GUTS_DATA_DIR").is_ok() {
244            self.storage.data_dir = env_config.storage.data_dir;
245        }
246
247        // Consensus environment variables
248        if std::env::var("GUTS_CONSENSUS_ENABLED").is_ok() {
249            self.consensus.enabled = env_config.consensus.enabled;
250        }
251        if std::env::var("GUTS_CONSENSUS_USE_SIMPLEX_BFT").is_ok() {
252            self.consensus.use_simplex_bft = env_config.consensus.use_simplex_bft;
253        }
254        if std::env::var("GUTS_CONSENSUS_BLOCK_TIME_MS").is_ok() {
255            self.consensus.block_time_ms = env_config.consensus.block_time_ms;
256        }
257        if std::env::var("GUTS_CONSENSUS_MAX_TXS_PER_BLOCK").is_ok() {
258            self.consensus.max_txs_per_block = env_config.consensus.max_txs_per_block;
259        }
260        if std::env::var("GUTS_CONSENSUS_MEMPOOL_MAX_TXS").is_ok() {
261            self.consensus.mempool_max_txs = env_config.consensus.mempool_max_txs;
262        }
263        if std::env::var("GUTS_CONSENSUS_MEMPOOL_TTL_SECS").is_ok() {
264            self.consensus.mempool_ttl_secs = env_config.consensus.mempool_ttl_secs;
265        }
266        if std::env::var("GUTS_CONSENSUS_GENESIS_FILE").is_ok() {
267            self.consensus.genesis_file = env_config.consensus.genesis_file;
268        }
269
270        Ok(())
271    }
272
273    /// Validate the configuration.
274    pub fn validate_config(&self) -> Result<(), ConfigError> {
275        // Use validator crate
276        self.validate()
277            .map_err(|e| ConfigError::Validation(e.to_string()))?;
278
279        // Custom validations
280        if let Some(ref key) = self.p2p.private_key {
281            validate_hex_key(key, "private_key", 64)?;
282        }
283
284        if let Some(ref share) = self.p2p.share {
285            validate_hex_key(share, "share", 64)?;
286        }
287
288        if let Some(ref polynomial) = self.p2p.polynomial {
289            validate_hex_key(polynomial, "polynomial", 64)?;
290        }
291
292        // Validate log level
293        let valid_levels = ["trace", "debug", "info", "warn", "error"];
294        if !valid_levels.contains(&self.logging.level.to_lowercase().as_str()) {
295            return Err(ConfigError::Invalid(format!(
296                "Invalid log level '{}'. Valid values: {:?}",
297                self.logging.level, valid_levels
298            )));
299        }
300
301        // Validate log format
302        let valid_formats = ["json", "pretty"];
303        if !valid_formats.contains(&self.logging.format.to_lowercase().as_str()) {
304            return Err(ConfigError::Invalid(format!(
305                "Invalid log format '{}'. Valid values: {:?}",
306                self.logging.format, valid_formats
307            )));
308        }
309
310        Ok(())
311    }
312}
313
314/// API server configuration.
315#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
316pub struct ApiConfig {
317    /// Listen address.
318    pub addr: SocketAddr,
319
320    /// Request timeout in seconds.
321    #[validate(range(min = 1, max = 3600))]
322    pub request_timeout_secs: u32,
323
324    /// Maximum request body size in bytes.
325    #[validate(range(min = 1024, max = 104857600))] // 1KB to 100MB
326    pub max_body_size: usize,
327
328    /// Maximum concurrent connections.
329    #[validate(range(min = 1, max = 100000))]
330    pub max_connections: u32,
331
332    /// Enable CORS.
333    pub cors_enabled: bool,
334
335    /// CORS allowed origins (empty = all).
336    pub cors_origins: Vec<String>,
337}
338
339impl Default for ApiConfig {
340    fn default() -> Self {
341        Self {
342            addr: "127.0.0.1:8080".parse().expect("Invalid default address"),
343            request_timeout_secs: 30,
344            max_body_size: 50 * 1024 * 1024, // 50MB
345            max_connections: 10000,
346            cors_enabled: true,
347            cors_origins: vec![],
348        }
349    }
350}
351
352/// P2P network configuration.
353#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
354pub struct P2pConfig {
355    /// Whether P2P is enabled.
356    pub enabled: bool,
357
358    /// P2P listen address.
359    pub addr: SocketAddr,
360
361    /// Ed25519 private key (hex encoded).
362    pub private_key: Option<String>,
363
364    /// BLS share (hex encoded).
365    pub share: Option<String>,
366
367    /// BLS polynomial (hex encoded).
368    pub polynomial: Option<String>,
369
370    /// Allowed peers (public keys).
371    pub allowed_peers: Vec<String>,
372
373    /// Bootstrap node addresses.
374    pub bootstrappers: Vec<String>,
375
376    /// Message backlog size.
377    #[validate(range(min = 16, max = 65536))]
378    pub message_backlog: usize,
379
380    /// Mailbox size.
381    #[validate(range(min = 16, max = 65536))]
382    pub mailbox_size: usize,
383
384    /// P2P operation timeout in seconds.
385    #[validate(range(min = 1, max = 300))]
386    pub timeout_secs: u32,
387}
388
389impl Default for P2pConfig {
390    fn default() -> Self {
391        Self {
392            enabled: false,
393            addr: "0.0.0.0:9000".parse().expect("Invalid default address"),
394            private_key: None,
395            share: None,
396            polynomial: None,
397            allowed_peers: Vec::new(),
398            bootstrappers: Vec::new(),
399            message_backlog: 1024,
400            mailbox_size: 1024,
401            timeout_secs: 10,
402        }
403    }
404}
405
406/// Consensus configuration.
407#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
408pub struct ConsensusConfig {
409    /// Whether consensus is enabled (false = single-node mode).
410    pub enabled: bool,
411
412    /// Use real Simplex BFT consensus (requires P2P to be enabled).
413    /// When false, uses the simulation-based consensus for development.
414    pub use_simplex_bft: bool,
415
416    /// Path to genesis file.
417    pub genesis_file: Option<PathBuf>,
418
419    /// Target block time in milliseconds.
420    #[validate(range(min = 100, max = 60000))]
421    pub block_time_ms: u64,
422
423    /// Maximum transactions per block.
424    #[validate(range(min = 1, max = 10000))]
425    pub max_txs_per_block: usize,
426
427    /// Maximum block size in bytes.
428    #[validate(range(min = 1024, max = 104857600))] // 1KB to 100MB
429    pub max_block_size: usize,
430
431    /// Mempool maximum transactions.
432    #[validate(range(min = 100, max = 1000000))]
433    pub mempool_max_txs: usize,
434
435    /// Mempool transaction TTL in seconds.
436    #[validate(range(min = 60, max = 3600))]
437    pub mempool_ttl_secs: u64,
438
439    /// View timeout multiplier for leader changes.
440    pub view_timeout_multiplier: f64,
441}
442
443impl Default for ConsensusConfig {
444    fn default() -> Self {
445        Self {
446            enabled: false,
447            use_simplex_bft: false,
448            genesis_file: None,
449            block_time_ms: 2000,
450            max_txs_per_block: 1000,
451            max_block_size: 10 * 1024 * 1024, // 10MB
452            mempool_max_txs: 10_000,
453            mempool_ttl_secs: 600,
454            view_timeout_multiplier: 2.0,
455        }
456    }
457}
458
459/// Metrics configuration.
460#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
461pub struct MetricsConfig {
462    /// Whether metrics are enabled.
463    pub enabled: bool,
464
465    /// Metrics endpoint address.
466    pub addr: SocketAddr,
467}
468
469impl Default for MetricsConfig {
470    fn default() -> Self {
471        Self {
472            enabled: true,
473            addr: "0.0.0.0:9090".parse().expect("Invalid default address"),
474        }
475    }
476}
477
478/// Logging configuration.
479#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
480pub struct LoggingConfig {
481    /// Log level (trace, debug, info, warn, error).
482    pub level: String,
483
484    /// Log format (json, pretty).
485    pub format: String,
486}
487
488impl Default for LoggingConfig {
489    fn default() -> Self {
490        Self {
491            level: "info".to_string(),
492            format: "json".to_string(),
493        }
494    }
495}
496
497/// Storage configuration.
498#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
499pub struct StorageConfig {
500    /// Data directory.
501    pub data_dir: PathBuf,
502
503    /// Enable data persistence (future feature).
504    pub persistent: bool,
505}
506
507impl Default for StorageConfig {
508    fn default() -> Self {
509        Self {
510            data_dir: PathBuf::from("./data"),
511            persistent: false,
512        }
513    }
514}
515
516/// Resilience configuration.
517#[derive(Debug, Clone, Default, Deserialize, Serialize, Validate)]
518pub struct ResilienceConfig {
519    /// Retry configuration.
520    #[validate(nested)]
521    #[serde(default)]
522    pub retry: RetryConfig,
523
524    /// Circuit breaker configuration.
525    #[validate(nested)]
526    #[serde(default)]
527    pub circuit_breaker: CircuitBreakerConfig,
528}
529
530/// Retry configuration.
531#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
532pub struct RetryConfig {
533    /// Maximum retry attempts.
534    #[validate(range(min = 0, max = 10))]
535    pub max_attempts: u32,
536
537    /// Initial delay in milliseconds.
538    #[validate(range(min = 10, max = 60000))]
539    pub initial_delay_ms: u64,
540
541    /// Maximum delay in milliseconds.
542    #[validate(range(min = 100, max = 300000))]
543    pub max_delay_ms: u64,
544
545    /// Backoff multiplier.
546    pub multiplier: f64,
547}
548
549impl Default for RetryConfig {
550    fn default() -> Self {
551        Self {
552            max_attempts: 3,
553            initial_delay_ms: 100,
554            max_delay_ms: 5000,
555            multiplier: 2.0,
556        }
557    }
558}
559
560impl RetryConfig {
561    /// Convert to RetryPolicy.
562    pub fn to_policy(&self) -> crate::resilience::RetryPolicy {
563        crate::resilience::RetryPolicy {
564            max_attempts: self.max_attempts,
565            initial_delay: Duration::from_millis(self.initial_delay_ms),
566            max_delay: Duration::from_millis(self.max_delay_ms),
567            multiplier: self.multiplier,
568            jitter: true,
569        }
570    }
571}
572
573/// Circuit breaker configuration.
574#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
575pub struct CircuitBreakerConfig {
576    /// Number of failures before opening.
577    #[validate(range(min = 1, max = 100))]
578    pub failure_threshold: u32,
579
580    /// Number of successes to close from half-open.
581    #[validate(range(min = 1, max = 100))]
582    pub success_threshold: u32,
583
584    /// Timeout in seconds before transitioning to half-open.
585    #[validate(range(min = 1, max = 3600))]
586    pub timeout_secs: u32,
587}
588
589impl Default for CircuitBreakerConfig {
590    fn default() -> Self {
591        Self {
592            failure_threshold: 5,
593            success_threshold: 3,
594            timeout_secs: 30,
595        }
596    }
597}
598
599impl CircuitBreakerConfig {
600    /// Convert to CircuitBreaker.
601    pub fn to_circuit_breaker(&self) -> crate::resilience::CircuitBreaker {
602        crate::resilience::CircuitBreaker::new(
603            self.failure_threshold,
604            self.success_threshold,
605            Duration::from_secs(self.timeout_secs as u64),
606        )
607    }
608}
609
610/// Validate a hexadecimal key.
611fn validate_hex_key(key: &str, name: &str, expected_len: usize) -> Result<(), ConfigError> {
612    // Remove 0x prefix if present
613    let key = key.strip_prefix("0x").unwrap_or(key);
614
615    if key.len() != expected_len {
616        return Err(ConfigError::Invalid(format!(
617            "{} must be {} hex characters, got {}",
618            name,
619            expected_len,
620            key.len()
621        )));
622    }
623
624    if !key.chars().all(|c| c.is_ascii_hexdigit()) {
625        return Err(ConfigError::Invalid(format!(
626            "{} contains non-hexadecimal characters",
627            name
628        )));
629    }
630
631    Ok(())
632}
633
634/// Legacy configuration for backwards compatibility.
635#[allow(dead_code)]
636#[derive(Debug, Clone, Deserialize, Serialize)]
637pub struct Config {
638    /// Ed25519 private key (hex encoded).
639    pub private_key: String,
640    /// BLS share (hex encoded).
641    pub share: String,
642    /// BLS polynomial (hex encoded).
643    pub polynomial: String,
644
645    /// P2P listen port.
646    pub port: u16,
647    /// Metrics HTTP port.
648    pub metrics_port: u16,
649    /// Data directory.
650    pub directory: String,
651    /// Number of worker threads.
652    pub worker_threads: usize,
653    /// Log level.
654    pub log_level: String,
655
656    /// Run in local mode.
657    pub local: bool,
658    /// Allowed peers (public keys).
659    pub allowed_peers: Vec<String>,
660    /// Bootstrap node addresses.
661    pub bootstrappers: Vec<String>,
662
663    /// Message backlog size.
664    pub message_backlog: usize,
665    /// Mailbox size.
666    pub mailbox_size: usize,
667    /// Deque size for pending messages.
668    pub deque_size: usize,
669}
670
671impl Default for Config {
672    fn default() -> Self {
673        Self {
674            private_key: String::new(),
675            share: String::new(),
676            polynomial: String::new(),
677            port: 9000,
678            metrics_port: 9090,
679            directory: "./data".to_string(),
680            worker_threads: 4,
681            log_level: "info".to_string(),
682            local: false,
683            allowed_peers: Vec::new(),
684            bootstrappers: Vec::new(),
685            message_backlog: 1024,
686            mailbox_size: 1024,
687            deque_size: 10,
688        }
689    }
690}
691
692/// Peer addresses for local mode.
693#[allow(dead_code)]
694#[derive(Debug, Clone, Deserialize, Serialize)]
695pub struct Peers {
696    /// Map of public key to socket address.
697    pub addresses: HashMap<String, SocketAddr>,
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn test_default_config() {
706        let config = NodeConfig::default();
707        assert!(config.validate_config().is_ok());
708    }
709
710    #[test]
711    fn test_hex_key_validation() {
712        // Valid key
713        assert!(validate_hex_key(
714            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
715            "test",
716            64
717        )
718        .is_ok());
719
720        // With 0x prefix
721        assert!(validate_hex_key(
722            "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
723            "test",
724            64
725        )
726        .is_ok());
727
728        // Wrong length
729        assert!(validate_hex_key("0123456789abcdef", "test", 64).is_err());
730
731        // Invalid characters
732        assert!(validate_hex_key(
733            "gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
734            "test",
735            64
736        )
737        .is_err());
738    }
739
740    #[test]
741    fn test_log_level_validation() {
742        let mut config = NodeConfig::default();
743
744        // Valid levels
745        for level in &["trace", "debug", "info", "warn", "error"] {
746            config.logging.level = level.to_string();
747            assert!(config.validate_config().is_ok());
748        }
749
750        // Invalid level
751        config.logging.level = "invalid".to_string();
752        assert!(config.validate_config().is_err());
753    }
754}