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")]
142 pub max_message_size: usize,
143
144 #[serde(default = "default_log_level")]
146 pub log_level: String,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct UpgradeConfig {
152 #[serde(default)]
154 pub channel: UpgradeChannel,
155
156 #[serde(default = "default_check_interval")]
158 pub check_interval_hours: u64,
159
160 #[serde(default = "default_github_repo")]
162 pub github_repo: String,
163
164 #[serde(default = "default_staged_rollout_hours")]
172 pub staged_rollout_hours: u64,
173
174 #[serde(default)]
181 pub stop_on_upgrade: bool,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
186#[serde(rename_all = "kebab-case")]
187pub enum EvmNetworkConfig {
188 #[default]
190 ArbitrumOne,
191 ArbitrumSepolia,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PaymentConfig {
202 #[serde(default = "default_cache_capacity")]
204 pub cache_capacity: usize,
205
206 #[serde(default)]
209 pub rewards_address: Option<String>,
210
211 #[serde(default)]
213 pub evm_network: EvmNetworkConfig,
214
215 #[serde(default = "default_metrics_port")]
218 pub metrics_port: u16,
219}
220
221impl Default for PaymentConfig {
222 fn default() -> Self {
223 Self {
224 cache_capacity: default_cache_capacity(),
225 rewards_address: None,
226 evm_network: EvmNetworkConfig::default(),
227 metrics_port: default_metrics_port(),
228 }
229 }
230}
231
232const fn default_metrics_port() -> u16 {
233 9100
234}
235
236const fn default_cache_capacity() -> usize {
237 100_000
238}
239
240impl Default for NodeConfig {
241 fn default() -> Self {
242 Self {
243 root_dir: default_root_dir(),
244 port: 0,
245 ipv4_only: false,
246 bootstrap: Vec::new(),
247 network_mode: NetworkMode::default(),
248 testnet: TestnetConfig::default(),
249 upgrade: UpgradeConfig::default(),
250 payment: PaymentConfig::default(),
251 bootstrap_cache: BootstrapCacheConfig::default(),
252 storage: StorageConfig::default(),
253 close_group_cache_dir: None,
254 max_message_size: default_max_message_size(),
255 log_level: default_log_level(),
256 }
257 }
258}
259
260impl NodeConfig {
261 #[must_use]
267 pub fn testnet() -> Self {
268 Self {
269 network_mode: NetworkMode::Testnet,
270 testnet: TestnetConfig::default(),
271 bootstrap: default_testnet_bootstrap(),
272 ..Self::default()
273 }
274 }
275
276 #[must_use]
280 pub fn development() -> Self {
281 Self {
282 network_mode: NetworkMode::Development,
283 testnet: TestnetConfig {
284 max_per_ip: Some(usize::MAX),
285 max_per_subnet: Some(usize::MAX),
286 },
287 ..Self::default()
288 }
289 }
290
291 #[must_use]
293 pub fn is_relaxed(&self) -> bool {
294 !matches!(self.network_mode, NetworkMode::Production)
295 }
296
297 pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
303 let content = std::fs::read_to_string(path)?;
304 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
305 }
306
307 pub fn to_file(&self, path: &std::path::Path) -> crate::Result<()> {
313 let content =
314 toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
315 std::fs::write(path, content)?;
316 Ok(())
317 }
318}
319
320impl Default for UpgradeConfig {
321 fn default() -> Self {
322 Self {
323 channel: UpgradeChannel::default(),
324 check_interval_hours: default_check_interval(),
325 github_repo: default_github_repo(),
326 staged_rollout_hours: default_staged_rollout_hours(),
327 stop_on_upgrade: false,
328 }
329 }
330}
331
332fn default_github_repo() -> String {
333 "WithAutonomi/ant-node".to_string()
334}
335
336#[must_use]
338pub fn default_root_dir() -> PathBuf {
339 directories::ProjectDirs::from("", "", "ant").map_or_else(
340 || PathBuf::from(".ant"),
341 |dirs| dirs.data_dir().to_path_buf(),
342 )
343}
344
345#[must_use]
350pub fn default_nodes_dir() -> PathBuf {
351 default_root_dir().join(NODES_SUBDIR)
352}
353
354fn default_max_message_size() -> usize {
355 crate::replication::config::MAX_REPLICATION_MESSAGE_SIZE
359 .max(crate::ant_protocol::MAX_WIRE_MESSAGE_SIZE)
360}
361
362fn default_log_level() -> String {
363 "info".to_string()
364}
365
366const fn default_check_interval() -> u64 {
367 1 }
369
370const fn default_staged_rollout_hours() -> u64 {
371 24 }
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct BootstrapCacheConfig {
386 #[serde(default = "default_bootstrap_cache_enabled")]
389 pub enabled: bool,
390
391 #[serde(default)]
394 pub cache_dir: Option<PathBuf>,
395
396 #[serde(default = "default_bootstrap_max_contacts")]
399 pub max_contacts: usize,
400
401 #[serde(default = "default_bootstrap_stale_days")]
405 pub stale_threshold_days: u64,
406}
407
408impl Default for BootstrapCacheConfig {
409 fn default() -> Self {
410 Self {
411 enabled: default_bootstrap_cache_enabled(),
412 cache_dir: None,
413 max_contacts: default_bootstrap_max_contacts(),
414 stale_threshold_days: default_bootstrap_stale_days(),
415 }
416 }
417}
418
419const fn default_bootstrap_cache_enabled() -> bool {
420 true
421}
422
423const fn default_bootstrap_max_contacts() -> usize {
424 10_000
425}
426
427const fn default_bootstrap_stale_days() -> u64 {
428 7
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct StorageConfig {
443 #[serde(default = "default_storage_enabled")]
446 pub enabled: bool,
447
448 #[serde(default = "default_storage_verify_on_read")]
451 pub verify_on_read: bool,
452
453 #[serde(default)]
459 pub db_size_gb: usize,
460
461 #[serde(default = "default_disk_reserve_mb")]
466 pub disk_reserve_mb: u64,
467}
468
469impl Default for StorageConfig {
470 fn default() -> Self {
471 Self {
472 enabled: default_storage_enabled(),
473 verify_on_read: default_storage_verify_on_read(),
474 db_size_gb: 0,
475 disk_reserve_mb: default_disk_reserve_mb(),
476 }
477 }
478}
479
480const fn default_disk_reserve_mb() -> u64 {
482 500
483}
484
485const fn default_storage_enabled() -> bool {
486 true
487}
488
489const fn default_storage_verify_on_read() -> bool {
490 true
491}
492
493pub const BOOTSTRAP_PEERS_FILENAME: &str = "bootstrap_peers.toml";
499
500pub const BOOTSTRAP_PEERS_ENV: &str = "ANT_BOOTSTRAP_PEERS_PATH";
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct BootstrapPeersConfig {
510 #[serde(default)]
512 pub peers: Vec<SocketAddr>,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
517pub enum BootstrapSource {
518 Cli,
520 ConfigFile,
522 AutoDiscovered(PathBuf),
524 None,
526}
527
528impl BootstrapPeersConfig {
529 pub fn from_file(path: &Path) -> crate::Result<Self> {
535 let content = std::fs::read_to_string(path)?;
536 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
537 }
538
539 #[must_use]
551 pub fn discover() -> Option<(Self, PathBuf)> {
552 let candidates = Self::search_paths();
553 for path in candidates {
554 if path.is_file() {
555 match Self::from_file(&path) {
556 Ok(config) if !config.peers.is_empty() => return Some((config, path)),
557 Ok(_) => {}
558 Err(err) => {
559 crate::logging::warn!(
560 "Failed to load bootstrap peers from {}: {err}",
561 path.display(),
562 );
563 }
564 }
565 }
566 }
567 None
568 }
569
570 fn search_paths() -> Vec<PathBuf> {
572 let mut paths = Vec::new();
573
574 if let Ok(env_path) = std::env::var(BOOTSTRAP_PEERS_ENV) {
576 paths.push(PathBuf::from(env_path));
577 }
578
579 if let Ok(exe) = std::env::current_exe() {
581 if let Some(exe_dir) = exe.parent() {
582 paths.push(exe_dir.join(BOOTSTRAP_PEERS_FILENAME));
583 }
584 }
585
586 if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "ant") {
588 paths.push(proj_dirs.config_dir().join(BOOTSTRAP_PEERS_FILENAME));
589 }
590
591 #[cfg(unix)]
593 {
594 paths.push(PathBuf::from("/etc/ant").join(BOOTSTRAP_PEERS_FILENAME));
595 }
596
597 paths
598 }
599}
600
601fn default_testnet_bootstrap() -> Vec<SocketAddr> {
607 vec![
608 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(165, 22, 4, 178), 12000)),
610 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(164, 92, 111, 156), 12000)),
612 ]
613}
614
615#[cfg(test)]
616#[allow(clippy::unwrap_used, clippy::expect_used)]
617mod tests {
618 use super::*;
619 use serial_test::serial;
620
621 #[test]
622 fn test_default_config_has_cache_capacity() {
623 let config = PaymentConfig::default();
624 assert!(config.cache_capacity > 0, "Cache capacity must be positive");
625 }
626
627 #[test]
628 fn test_default_evm_network() {
629 use crate::payment::EvmVerifierConfig;
630 let _config = EvmVerifierConfig::default();
631 }
633
634 #[test]
635 fn test_bootstrap_peers_parse_valid_toml() {
636 let toml_str = r#"
637 peers = [
638 "127.0.0.1:10000",
639 "192.168.1.1:10001",
640 ]
641 "#;
642 let config: BootstrapPeersConfig =
643 toml::from_str(toml_str).expect("valid TOML should parse");
644 assert_eq!(config.peers.len(), 2);
645 assert_eq!(config.peers[0].port(), 10000);
646 assert_eq!(config.peers[1].port(), 10001);
647 }
648
649 #[test]
650 fn test_bootstrap_peers_parse_empty_peers() {
651 let toml_str = r"peers = []";
652 let config: BootstrapPeersConfig =
653 toml::from_str(toml_str).expect("empty peers should parse");
654 assert!(config.peers.is_empty());
655 }
656
657 #[test]
658 fn test_bootstrap_peers_parse_missing_peers_field() {
659 let toml_str = "";
660 let config: BootstrapPeersConfig =
661 toml::from_str(toml_str).expect("missing field should use default");
662 assert!(config.peers.is_empty());
663 }
664
665 #[test]
666 fn test_bootstrap_peers_from_file() {
667 let dir = tempfile::tempdir().expect("create temp dir");
668 let path = dir.path().join("bootstrap_peers.toml");
669 std::fs::write(&path, r#"peers = ["10.0.0.1:10000", "10.0.0.2:10000"]"#)
670 .expect("write file");
671
672 let config = BootstrapPeersConfig::from_file(&path).expect("load from file");
673 assert_eq!(config.peers.len(), 2);
674 }
675
676 #[test]
677 fn test_bootstrap_peers_from_file_invalid_toml() {
678 let dir = tempfile::tempdir().expect("create temp dir");
679 let path = dir.path().join("bootstrap_peers.toml");
680 std::fs::write(&path, "not valid toml [[[").expect("write file");
681
682 assert!(BootstrapPeersConfig::from_file(&path).is_err());
683 }
684
685 #[test]
688 #[serial]
689 fn test_bootstrap_peers_discover_env_var() {
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 = ["10.0.0.1:10000"]"#).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 let (config, discovered_path) = result.expect("should discover from env var");
701 assert_eq!(config.peers.len(), 1);
702 assert_eq!(discovered_path, path);
703 }
704
705 {
707 let dir = tempfile::tempdir().expect("create temp dir");
708 let path = dir.path().join("bootstrap_peers.toml");
709 std::fs::write(&path, r"peers = []").expect("write file");
710
711 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
712 let result = BootstrapPeersConfig::discover();
713 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
714
715 assert!(result.is_none(), "empty peers file should be skipped");
716 }
717 }
718
719 #[test]
720 fn test_bootstrap_peers_search_paths_contains_exe_dir() {
721 let paths = BootstrapPeersConfig::search_paths();
722 assert!(
724 paths
725 .iter()
726 .any(|p| p.file_name().is_some_and(|f| f == BOOTSTRAP_PEERS_FILENAME)),
727 "search paths should include a candidate with the bootstrap peers filename"
728 );
729 }
730}