1use serde::{Deserialize, Serialize};
4use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
5use std::path::{Path, PathBuf};
6
7pub const NODE_IDENTITY_FILENAME: &str = "node_identity.key";
9
10pub const NODES_SUBDIR: &str = "nodes";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum UpgradeChannel {
17 #[default]
19 Stable,
20 Beta,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum NetworkMode {
28 #[default]
30 Production,
31 Testnet,
34 Development,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TestnetConfig {
46 #[serde(default = "default_testnet_max_per_ip")]
49 pub max_per_ip: Option<usize>,
50
51 #[serde(default = "default_testnet_max_per_subnet")]
54 pub max_per_subnet: Option<usize>,
55}
56
57impl Default for TestnetConfig {
58 fn default() -> Self {
59 Self {
60 max_per_ip: default_testnet_max_per_ip(),
61 max_per_subnet: default_testnet_max_per_subnet(),
62 }
63 }
64}
65
66#[allow(clippy::unnecessary_wraps)]
69const fn default_testnet_max_per_ip() -> Option<usize> {
70 Some(usize::MAX)
71}
72
73#[allow(clippy::unnecessary_wraps)]
74const fn default_testnet_max_per_subnet() -> Option<usize> {
75 Some(usize::MAX)
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct NodeConfig {
81 #[serde(default = "default_root_dir")]
83 pub root_dir: PathBuf,
84
85 #[serde(default)]
87 pub port: u16,
88
89 #[serde(default)]
95 pub ipv4_only: bool,
96
97 #[serde(default)]
99 pub bootstrap: Vec<SocketAddr>,
100
101 #[serde(default)]
103 pub network_mode: NetworkMode,
104
105 #[serde(default)]
108 pub testnet: TestnetConfig,
109
110 #[serde(default)]
112 pub upgrade: UpgradeConfig,
113
114 #[serde(default)]
116 pub payment: PaymentConfig,
117
118 #[serde(default)]
120 pub bootstrap_cache: BootstrapCacheConfig,
121
122 #[serde(default)]
124 pub storage: StorageConfig,
125
126 #[serde(default)]
131 pub close_group_cache_dir: Option<PathBuf>,
132
133 #[serde(default = "default_max_message_size")]
140 pub max_message_size: usize,
141
142 #[serde(default = "default_log_level")]
144 pub log_level: String,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct UpgradeConfig {
150 #[serde(default)]
152 pub channel: UpgradeChannel,
153
154 #[serde(default = "default_check_interval")]
156 pub check_interval_hours: u64,
157
158 #[serde(default = "default_github_repo")]
160 pub github_repo: String,
161
162 #[serde(default = "default_staged_rollout_hours")]
170 pub staged_rollout_hours: u64,
171
172 #[serde(default)]
179 pub stop_on_upgrade: bool,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
184#[serde(rename_all = "kebab-case")]
185pub enum EvmNetworkConfig {
186 #[default]
188 ArbitrumOne,
189 ArbitrumSepolia,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct PaymentConfig {
200 #[serde(default = "default_cache_capacity")]
202 pub cache_capacity: usize,
203
204 #[serde(default)]
207 pub rewards_address: Option<String>,
208
209 #[serde(default)]
211 pub evm_network: EvmNetworkConfig,
212
213 #[serde(default = "default_metrics_port")]
216 pub metrics_port: u16,
217}
218
219impl Default for PaymentConfig {
220 fn default() -> Self {
221 Self {
222 cache_capacity: default_cache_capacity(),
223 rewards_address: None,
224 evm_network: EvmNetworkConfig::default(),
225 metrics_port: default_metrics_port(),
226 }
227 }
228}
229
230const fn default_metrics_port() -> u16 {
231 9100
232}
233
234const fn default_cache_capacity() -> usize {
235 100_000
236}
237
238impl Default for NodeConfig {
239 fn default() -> Self {
240 Self {
241 root_dir: default_root_dir(),
242 port: 0,
243 ipv4_only: false,
244 bootstrap: Vec::new(),
245 network_mode: NetworkMode::default(),
246 testnet: TestnetConfig::default(),
247 upgrade: UpgradeConfig::default(),
248 payment: PaymentConfig::default(),
249 bootstrap_cache: BootstrapCacheConfig::default(),
250 storage: StorageConfig::default(),
251 close_group_cache_dir: None,
252 max_message_size: default_max_message_size(),
253 log_level: default_log_level(),
254 }
255 }
256}
257
258impl NodeConfig {
259 #[must_use]
265 pub fn testnet() -> Self {
266 Self {
267 network_mode: NetworkMode::Testnet,
268 testnet: TestnetConfig::default(),
269 bootstrap: default_testnet_bootstrap(),
270 ..Self::default()
271 }
272 }
273
274 #[must_use]
278 pub fn development() -> Self {
279 Self {
280 network_mode: NetworkMode::Development,
281 testnet: TestnetConfig {
282 max_per_ip: Some(usize::MAX),
283 max_per_subnet: Some(usize::MAX),
284 },
285 ..Self::default()
286 }
287 }
288
289 #[must_use]
291 pub fn is_relaxed(&self) -> bool {
292 !matches!(self.network_mode, NetworkMode::Production)
293 }
294
295 pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
301 let content = std::fs::read_to_string(path)?;
302 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
303 }
304
305 pub fn to_file(&self, path: &std::path::Path) -> crate::Result<()> {
311 let content =
312 toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
313 std::fs::write(path, content)?;
314 Ok(())
315 }
316}
317
318impl Default for UpgradeConfig {
319 fn default() -> Self {
320 Self {
321 channel: UpgradeChannel::default(),
322 check_interval_hours: default_check_interval(),
323 github_repo: default_github_repo(),
324 staged_rollout_hours: default_staged_rollout_hours(),
325 stop_on_upgrade: false,
326 }
327 }
328}
329
330fn default_github_repo() -> String {
331 "WithAutonomi/ant-node".to_string()
332}
333
334#[must_use]
336pub fn default_root_dir() -> PathBuf {
337 directories::ProjectDirs::from("", "", "ant").map_or_else(
338 || PathBuf::from(".ant"),
339 |dirs| dirs.data_dir().to_path_buf(),
340 )
341}
342
343#[must_use]
348pub fn default_nodes_dir() -> PathBuf {
349 default_root_dir().join(NODES_SUBDIR)
350}
351
352fn default_max_message_size() -> usize {
353 crate::ant_protocol::MAX_WIRE_MESSAGE_SIZE
354}
355
356fn default_log_level() -> String {
357 "info".to_string()
358}
359
360const fn default_check_interval() -> u64 {
361 1 }
363
364const fn default_staged_rollout_hours() -> u64 {
365 24 }
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct BootstrapCacheConfig {
380 #[serde(default = "default_bootstrap_cache_enabled")]
383 pub enabled: bool,
384
385 #[serde(default)]
388 pub cache_dir: Option<PathBuf>,
389
390 #[serde(default = "default_bootstrap_max_contacts")]
393 pub max_contacts: usize,
394
395 #[serde(default = "default_bootstrap_stale_days")]
399 pub stale_threshold_days: u64,
400}
401
402impl Default for BootstrapCacheConfig {
403 fn default() -> Self {
404 Self {
405 enabled: default_bootstrap_cache_enabled(),
406 cache_dir: None,
407 max_contacts: default_bootstrap_max_contacts(),
408 stale_threshold_days: default_bootstrap_stale_days(),
409 }
410 }
411}
412
413const fn default_bootstrap_cache_enabled() -> bool {
414 true
415}
416
417const fn default_bootstrap_max_contacts() -> usize {
418 10_000
419}
420
421const fn default_bootstrap_stale_days() -> u64 {
422 7
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct StorageConfig {
437 #[serde(default = "default_storage_enabled")]
440 pub enabled: bool,
441
442 #[serde(default)]
445 pub max_chunks: usize,
446
447 #[serde(default = "default_storage_verify_on_read")]
450 pub verify_on_read: bool,
451
452 #[serde(default)]
456 pub db_size_gb: usize,
457}
458
459impl Default for StorageConfig {
460 fn default() -> Self {
461 Self {
462 enabled: default_storage_enabled(),
463 max_chunks: 0,
464 verify_on_read: default_storage_verify_on_read(),
465 db_size_gb: 0,
466 }
467 }
468}
469
470const fn default_storage_enabled() -> bool {
471 true
472}
473
474const fn default_storage_verify_on_read() -> bool {
475 true
476}
477
478pub const BOOTSTRAP_PEERS_FILENAME: &str = "bootstrap_peers.toml";
484
485pub const BOOTSTRAP_PEERS_ENV: &str = "ANT_BOOTSTRAP_PEERS_PATH";
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct BootstrapPeersConfig {
495 #[serde(default)]
497 pub peers: Vec<SocketAddr>,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq)]
502pub enum BootstrapSource {
503 Cli,
505 ConfigFile,
507 AutoDiscovered(PathBuf),
509 None,
511}
512
513impl BootstrapPeersConfig {
514 pub fn from_file(path: &Path) -> crate::Result<Self> {
520 let content = std::fs::read_to_string(path)?;
521 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
522 }
523
524 #[must_use]
536 pub fn discover() -> Option<(Self, PathBuf)> {
537 let candidates = Self::search_paths();
538 for path in candidates {
539 if path.is_file() {
540 match Self::from_file(&path) {
541 Ok(config) if !config.peers.is_empty() => return Some((config, path)),
542 Ok(_) => {}
543 Err(err) => {
544 eprintln!(
545 "Warning: failed to load bootstrap peers from {}: {err}",
546 path.display(),
547 );
548 }
549 }
550 }
551 }
552 None
553 }
554
555 fn search_paths() -> Vec<PathBuf> {
557 let mut paths = Vec::new();
558
559 if let Ok(env_path) = std::env::var(BOOTSTRAP_PEERS_ENV) {
561 paths.push(PathBuf::from(env_path));
562 }
563
564 if let Ok(exe) = std::env::current_exe() {
566 if let Some(exe_dir) = exe.parent() {
567 paths.push(exe_dir.join(BOOTSTRAP_PEERS_FILENAME));
568 }
569 }
570
571 if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "ant") {
573 paths.push(proj_dirs.config_dir().join(BOOTSTRAP_PEERS_FILENAME));
574 }
575
576 #[cfg(unix)]
578 {
579 paths.push(PathBuf::from("/etc/ant").join(BOOTSTRAP_PEERS_FILENAME));
580 }
581
582 paths
583 }
584}
585
586fn default_testnet_bootstrap() -> Vec<SocketAddr> {
592 vec![
593 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(165, 22, 4, 178), 12000)),
595 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(164, 92, 111, 156), 12000)),
597 ]
598}
599
600#[cfg(test)]
601#[allow(clippy::unwrap_used, clippy::expect_used)]
602mod tests {
603 use super::*;
604 use serial_test::serial;
605
606 #[test]
607 fn test_default_config_has_cache_capacity() {
608 let config = PaymentConfig::default();
609 assert!(config.cache_capacity > 0, "Cache capacity must be positive");
610 }
611
612 #[test]
613 fn test_default_evm_network() {
614 use crate::payment::EvmVerifierConfig;
615 let _config = EvmVerifierConfig::default();
616 }
618
619 #[test]
620 fn test_bootstrap_peers_parse_valid_toml() {
621 let toml_str = r#"
622 peers = [
623 "127.0.0.1:10000",
624 "192.168.1.1:10001",
625 ]
626 "#;
627 let config: BootstrapPeersConfig =
628 toml::from_str(toml_str).expect("valid TOML should parse");
629 assert_eq!(config.peers.len(), 2);
630 assert_eq!(config.peers[0].port(), 10000);
631 assert_eq!(config.peers[1].port(), 10001);
632 }
633
634 #[test]
635 fn test_bootstrap_peers_parse_empty_peers() {
636 let toml_str = r"peers = []";
637 let config: BootstrapPeersConfig =
638 toml::from_str(toml_str).expect("empty peers should parse");
639 assert!(config.peers.is_empty());
640 }
641
642 #[test]
643 fn test_bootstrap_peers_parse_missing_peers_field() {
644 let toml_str = "";
645 let config: BootstrapPeersConfig =
646 toml::from_str(toml_str).expect("missing field should use default");
647 assert!(config.peers.is_empty());
648 }
649
650 #[test]
651 fn test_bootstrap_peers_from_file() {
652 let dir = tempfile::tempdir().expect("create temp dir");
653 let path = dir.path().join("bootstrap_peers.toml");
654 std::fs::write(&path, r#"peers = ["10.0.0.1:10000", "10.0.0.2:10000"]"#)
655 .expect("write file");
656
657 let config = BootstrapPeersConfig::from_file(&path).expect("load from file");
658 assert_eq!(config.peers.len(), 2);
659 }
660
661 #[test]
662 fn test_bootstrap_peers_from_file_invalid_toml() {
663 let dir = tempfile::tempdir().expect("create temp dir");
664 let path = dir.path().join("bootstrap_peers.toml");
665 std::fs::write(&path, "not valid toml [[[").expect("write file");
666
667 assert!(BootstrapPeersConfig::from_file(&path).is_err());
668 }
669
670 #[test]
673 #[serial]
674 fn test_bootstrap_peers_discover_env_var() {
675 {
677 let dir = tempfile::tempdir().expect("create temp dir");
678 let path = dir.path().join("bootstrap_peers.toml");
679 std::fs::write(&path, r#"peers = ["10.0.0.1:10000"]"#).expect("write file");
680
681 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
682 let result = BootstrapPeersConfig::discover();
683 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
684
685 let (config, discovered_path) = result.expect("should discover from env var");
686 assert_eq!(config.peers.len(), 1);
687 assert_eq!(discovered_path, path);
688 }
689
690 {
692 let dir = tempfile::tempdir().expect("create temp dir");
693 let path = dir.path().join("bootstrap_peers.toml");
694 std::fs::write(&path, r"peers = []").expect("write file");
695
696 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
697 let result = BootstrapPeersConfig::discover();
698 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
699
700 assert!(result.is_none(), "empty peers file should be skipped");
701 }
702 }
703
704 #[test]
705 fn test_bootstrap_peers_search_paths_contains_exe_dir() {
706 let paths = BootstrapPeersConfig::search_paths();
707 assert!(
709 paths
710 .iter()
711 .any(|p| p.file_name().is_some_and(|f| f == BOOTSTRAP_PEERS_FILENAME)),
712 "search paths should include a candidate with the bootstrap peers filename"
713 );
714 }
715}