1use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::net::SocketAddr;
34use std::path::PathBuf;
35use std::time::Duration;
36use validator::Validate;
37
38#[derive(Debug, thiserror::Error)]
40pub enum ConfigError {
41 #[error("Invalid configuration: {0}")]
43 Invalid(String),
44
45 #[error("Missing required configuration: {0}")]
47 Missing(String),
48
49 #[error("Failed to parse environment variable {key}: {message}")]
51 EnvParse { key: String, message: String },
52
53 #[error("Failed to load configuration file: {0}")]
55 FileLoad(String),
56
57 #[error("Validation failed: {0}")]
59 Validation(String),
60}
61
62#[derive(Debug, Clone, Default, Deserialize, Serialize, Validate)]
64pub struct NodeConfig {
65 #[validate(nested)]
67 #[serde(default)]
68 pub api: ApiConfig,
69
70 #[validate(nested)]
72 #[serde(default)]
73 pub p2p: P2pConfig,
74
75 #[validate(nested)]
77 #[serde(default)]
78 pub consensus: ConsensusConfig,
79
80 #[validate(nested)]
82 #[serde(default)]
83 pub metrics: MetricsConfig,
84
85 #[validate(nested)]
87 #[serde(default)]
88 pub logging: LoggingConfig,
89
90 #[validate(nested)]
92 #[serde(default)]
93 pub storage: StorageConfig,
94
95 #[validate(nested)]
97 #[serde(default)]
98 pub resilience: ResilienceConfig,
99}
100
101impl NodeConfig {
102 pub fn from_env() -> Result<Self, ConfigError> {
104 let mut config = Self::default();
105
106 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 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 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 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 if let Ok(dir) = std::env::var("GUTS_DATA_DIR") {
161 config.storage.data_dir = PathBuf::from(dir);
162 }
163
164 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 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 pub fn merge_env(&mut self) -> Result<(), ConfigError> {
222 let env_config = Self::from_env()?;
223
224 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 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 pub fn validate_config(&self) -> Result<(), ConfigError> {
275 self.validate()
277 .map_err(|e| ConfigError::Validation(e.to_string()))?;
278
279 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 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 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#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
316pub struct ApiConfig {
317 pub addr: SocketAddr,
319
320 #[validate(range(min = 1, max = 3600))]
322 pub request_timeout_secs: u32,
323
324 #[validate(range(min = 1024, max = 104857600))] pub max_body_size: usize,
327
328 #[validate(range(min = 1, max = 100000))]
330 pub max_connections: u32,
331
332 pub cors_enabled: bool,
334
335 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, max_connections: 10000,
346 cors_enabled: true,
347 cors_origins: vec![],
348 }
349 }
350}
351
352#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
354pub struct P2pConfig {
355 pub enabled: bool,
357
358 pub addr: SocketAddr,
360
361 pub private_key: Option<String>,
363
364 pub share: Option<String>,
366
367 pub polynomial: Option<String>,
369
370 pub allowed_peers: Vec<String>,
372
373 pub bootstrappers: Vec<String>,
375
376 #[validate(range(min = 16, max = 65536))]
378 pub message_backlog: usize,
379
380 #[validate(range(min = 16, max = 65536))]
382 pub mailbox_size: usize,
383
384 #[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#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
408pub struct ConsensusConfig {
409 pub enabled: bool,
411
412 pub use_simplex_bft: bool,
415
416 pub genesis_file: Option<PathBuf>,
418
419 #[validate(range(min = 100, max = 60000))]
421 pub block_time_ms: u64,
422
423 #[validate(range(min = 1, max = 10000))]
425 pub max_txs_per_block: usize,
426
427 #[validate(range(min = 1024, max = 104857600))] pub max_block_size: usize,
430
431 #[validate(range(min = 100, max = 1000000))]
433 pub mempool_max_txs: usize,
434
435 #[validate(range(min = 60, max = 3600))]
437 pub mempool_ttl_secs: u64,
438
439 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, mempool_max_txs: 10_000,
453 mempool_ttl_secs: 600,
454 view_timeout_multiplier: 2.0,
455 }
456 }
457}
458
459#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
461pub struct MetricsConfig {
462 pub enabled: bool,
464
465 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#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
480pub struct LoggingConfig {
481 pub level: String,
483
484 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#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
499pub struct StorageConfig {
500 pub data_dir: PathBuf,
502
503 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#[derive(Debug, Clone, Default, Deserialize, Serialize, Validate)]
518pub struct ResilienceConfig {
519 #[validate(nested)]
521 #[serde(default)]
522 pub retry: RetryConfig,
523
524 #[validate(nested)]
526 #[serde(default)]
527 pub circuit_breaker: CircuitBreakerConfig,
528}
529
530#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
532pub struct RetryConfig {
533 #[validate(range(min = 0, max = 10))]
535 pub max_attempts: u32,
536
537 #[validate(range(min = 10, max = 60000))]
539 pub initial_delay_ms: u64,
540
541 #[validate(range(min = 100, max = 300000))]
543 pub max_delay_ms: u64,
544
545 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 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#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
575pub struct CircuitBreakerConfig {
576 #[validate(range(min = 1, max = 100))]
578 pub failure_threshold: u32,
579
580 #[validate(range(min = 1, max = 100))]
582 pub success_threshold: u32,
583
584 #[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 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
610fn validate_hex_key(key: &str, name: &str, expected_len: usize) -> Result<(), ConfigError> {
612 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#[allow(dead_code)]
636#[derive(Debug, Clone, Deserialize, Serialize)]
637pub struct Config {
638 pub private_key: String,
640 pub share: String,
642 pub polynomial: String,
644
645 pub port: u16,
647 pub metrics_port: u16,
649 pub directory: String,
651 pub worker_threads: usize,
653 pub log_level: String,
655
656 pub local: bool,
658 pub allowed_peers: Vec<String>,
660 pub bootstrappers: Vec<String>,
662
663 pub message_backlog: usize,
665 pub mailbox_size: usize,
667 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#[allow(dead_code)]
694#[derive(Debug, Clone, Deserialize, Serialize)]
695pub struct Peers {
696 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 assert!(validate_hex_key(
714 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
715 "test",
716 64
717 )
718 .is_ok());
719
720 assert!(validate_hex_key(
722 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
723 "test",
724 64
725 )
726 .is_ok());
727
728 assert!(validate_hex_key("0123456789abcdef", "test", 64).is_err());
730
731 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 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 config.logging.level = "invalid".to_string();
752 assert!(config.validate_config().is_err());
753 }
754}