1use evmlib::Network as EvmNetwork;
4use serde::{Deserialize, Serialize};
5use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
6use std::path::{Path, PathBuf};
7
8pub const NODE_IDENTITY_FILENAME: &str = "node_identity.key";
10
11pub const NODES_SUBDIR: &str = "nodes";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum UpgradeChannel {
18 #[default]
20 Stable,
21 Beta,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum NetworkMode {
29 #[default]
31 Production,
32 Testnet,
35 Development,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct TestnetConfig {
47 #[serde(default = "default_testnet_max_per_ip")]
50 pub max_per_ip: Option<usize>,
51
52 #[serde(default = "default_testnet_max_per_subnet")]
55 pub max_per_subnet: Option<usize>,
56}
57
58impl Default for TestnetConfig {
59 fn default() -> Self {
60 Self {
61 max_per_ip: default_testnet_max_per_ip(),
62 max_per_subnet: default_testnet_max_per_subnet(),
63 }
64 }
65}
66
67#[allow(clippy::unnecessary_wraps)]
70const fn default_testnet_max_per_ip() -> Option<usize> {
71 Some(usize::MAX)
72}
73
74#[allow(clippy::unnecessary_wraps)]
75const fn default_testnet_max_per_subnet() -> Option<usize> {
76 Some(usize::MAX)
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct NodeConfig {
82 #[serde(default = "default_root_dir")]
84 pub root_dir: PathBuf,
85
86 #[serde(default)]
88 pub port: u16,
89
90 #[serde(default)]
96 pub ipv4_only: bool,
97
98 #[serde(default)]
100 pub bootstrap: Vec<SocketAddr>,
101
102 #[serde(default)]
104 pub network_mode: NetworkMode,
105
106 #[serde(default)]
109 pub testnet: TestnetConfig,
110
111 #[serde(default)]
113 pub upgrade: UpgradeConfig,
114
115 #[serde(default)]
117 pub payment: PaymentConfig,
118
119 #[serde(default)]
121 pub bootstrap_cache: BootstrapCacheConfig,
122
123 #[serde(default)]
125 pub storage: StorageConfig,
126
127 #[serde(default)]
132 pub close_group_cache_dir: Option<PathBuf>,
133
134 #[serde(default = "default_max_message_size")]
143 pub max_message_size: usize,
144
145 #[serde(default = "default_log_level")]
147 pub log_level: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct UpgradeConfig {
153 #[serde(default)]
155 pub channel: UpgradeChannel,
156
157 #[serde(default = "default_check_interval")]
159 pub check_interval_hours: u64,
160
161 #[serde(default = "default_github_repo")]
163 pub github_repo: String,
164
165 #[serde(default = "default_staged_rollout_hours")]
173 pub staged_rollout_hours: u64,
174
175 #[serde(default)]
182 pub stop_on_upgrade: bool,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
193#[serde(rename_all = "kebab-case", tag = "type")]
194pub enum EvmNetworkConfig {
195 #[default]
197 ArbitrumOne,
198 ArbitrumSepolia,
200 Custom {
203 rpc_url: String,
205 payment_token_address: String,
207 payment_vault_address: String,
209 },
210}
211
212impl EvmNetworkConfig {
213 #[must_use]
216 pub fn into_evm_network(self) -> EvmNetwork {
217 match self {
218 Self::ArbitrumOne => EvmNetwork::ArbitrumOne,
219 Self::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest,
220 Self::Custom {
221 rpc_url,
222 payment_token_address,
223 payment_vault_address,
224 } => EvmNetwork::new_custom(&rpc_url, &payment_token_address, &payment_vault_address),
225 }
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct PaymentConfig {
236 #[serde(default = "default_cache_capacity")]
238 pub cache_capacity: usize,
239
240 #[serde(default)]
243 pub rewards_address: Option<String>,
244
245 #[serde(default)]
247 pub evm_network: EvmNetworkConfig,
248
249 #[serde(default = "default_metrics_port")]
252 pub metrics_port: u16,
253}
254
255impl Default for PaymentConfig {
256 fn default() -> Self {
257 Self {
258 cache_capacity: default_cache_capacity(),
259 rewards_address: None,
260 evm_network: EvmNetworkConfig::default(),
261 metrics_port: default_metrics_port(),
262 }
263 }
264}
265
266const fn default_metrics_port() -> u16 {
267 9100
268}
269
270const fn default_cache_capacity() -> usize {
271 100_000
272}
273
274impl Default for NodeConfig {
275 fn default() -> Self {
276 Self {
277 root_dir: default_root_dir(),
278 port: 0,
279 ipv4_only: false,
280 bootstrap: Vec::new(),
281 network_mode: NetworkMode::default(),
282 testnet: TestnetConfig::default(),
283 upgrade: UpgradeConfig::default(),
284 payment: PaymentConfig::default(),
285 bootstrap_cache: BootstrapCacheConfig::default(),
286 storage: StorageConfig::default(),
287 close_group_cache_dir: None,
288 max_message_size: default_max_message_size(),
289 log_level: default_log_level(),
290 }
291 }
292}
293
294impl NodeConfig {
295 #[must_use]
301 pub fn testnet() -> Self {
302 Self {
303 network_mode: NetworkMode::Testnet,
304 testnet: TestnetConfig::default(),
305 bootstrap: default_testnet_bootstrap(),
306 ..Self::default()
307 }
308 }
309
310 #[must_use]
314 pub fn development() -> Self {
315 Self {
316 network_mode: NetworkMode::Development,
317 testnet: TestnetConfig {
318 max_per_ip: Some(usize::MAX),
319 max_per_subnet: Some(usize::MAX),
320 },
321 ..Self::default()
322 }
323 }
324
325 #[must_use]
327 pub fn is_relaxed(&self) -> bool {
328 !matches!(self.network_mode, NetworkMode::Production)
329 }
330
331 pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
337 let content = std::fs::read_to_string(path)?;
338 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
339 }
340
341 pub fn to_file(&self, path: &std::path::Path) -> crate::Result<()> {
347 let content =
348 toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
349 std::fs::write(path, content)?;
350 Ok(())
351 }
352}
353
354impl Default for UpgradeConfig {
355 fn default() -> Self {
356 Self {
357 channel: UpgradeChannel::default(),
358 check_interval_hours: default_check_interval(),
359 github_repo: default_github_repo(),
360 staged_rollout_hours: default_staged_rollout_hours(),
361 stop_on_upgrade: false,
362 }
363 }
364}
365
366fn default_github_repo() -> String {
367 "WithAutonomi/ant-node".to_string()
368}
369
370#[must_use]
372pub fn default_root_dir() -> PathBuf {
373 directories::ProjectDirs::from("", "", "ant").map_or_else(
374 || PathBuf::from(".ant"),
375 |dirs| dirs.data_dir().to_path_buf(),
376 )
377}
378
379#[must_use]
384pub fn default_nodes_dir() -> PathBuf {
385 default_root_dir().join(NODES_SUBDIR)
386}
387
388fn default_max_message_size() -> usize {
389 crate::replication::config::MAX_REPLICATION_MESSAGE_SIZE
393 .max(crate::ant_protocol::MAX_WIRE_MESSAGE_SIZE)
394}
395
396fn default_log_level() -> String {
397 "info".to_string()
398}
399
400const fn default_check_interval() -> u64 {
401 1 }
403
404const fn default_staged_rollout_hours() -> u64 {
405 24 }
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct BootstrapCacheConfig {
420 #[serde(default = "default_bootstrap_cache_enabled")]
423 pub enabled: bool,
424
425 #[serde(default)]
428 pub cache_dir: Option<PathBuf>,
429
430 #[serde(default = "default_bootstrap_max_contacts")]
433 pub max_contacts: usize,
434
435 #[serde(default = "default_bootstrap_stale_days")]
439 pub stale_threshold_days: u64,
440}
441
442impl Default for BootstrapCacheConfig {
443 fn default() -> Self {
444 Self {
445 enabled: default_bootstrap_cache_enabled(),
446 cache_dir: None,
447 max_contacts: default_bootstrap_max_contacts(),
448 stale_threshold_days: default_bootstrap_stale_days(),
449 }
450 }
451}
452
453const fn default_bootstrap_cache_enabled() -> bool {
454 true
455}
456
457const fn default_bootstrap_max_contacts() -> usize {
458 10_000
459}
460
461const fn default_bootstrap_stale_days() -> u64 {
462 7
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct StorageConfig {
477 #[serde(default = "default_storage_enabled")]
480 pub enabled: bool,
481
482 #[serde(default = "default_storage_verify_on_read")]
485 pub verify_on_read: bool,
486
487 #[serde(default)]
493 pub db_size_gb: usize,
494
495 #[serde(default = "default_disk_reserve_mb")]
500 pub disk_reserve_mb: u64,
501}
502
503impl Default for StorageConfig {
504 fn default() -> Self {
505 Self {
506 enabled: default_storage_enabled(),
507 verify_on_read: default_storage_verify_on_read(),
508 db_size_gb: 0,
509 disk_reserve_mb: default_disk_reserve_mb(),
510 }
511 }
512}
513
514const fn default_disk_reserve_mb() -> u64 {
516 500
517}
518
519const fn default_storage_enabled() -> bool {
520 true
521}
522
523const fn default_storage_verify_on_read() -> bool {
524 true
525}
526
527pub const BOOTSTRAP_PEERS_FILENAME: &str = "bootstrap_peers.toml";
533
534pub const BOOTSTRAP_PEERS_ENV: &str = "ANT_BOOTSTRAP_PEERS_PATH";
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct BootstrapPeersConfig {
544 #[serde(default)]
546 pub peers: Vec<SocketAddr>,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq)]
551pub enum BootstrapSource {
552 Cli,
554 ConfigFile,
556 AutoDiscovered(PathBuf),
558 None,
560}
561
562impl BootstrapPeersConfig {
563 pub fn from_file(path: &Path) -> crate::Result<Self> {
569 let content = std::fs::read_to_string(path)?;
570 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
571 }
572
573 #[must_use]
585 pub fn discover() -> Option<(Self, PathBuf)> {
586 let candidates = Self::search_paths();
587 for path in candidates {
588 if path.is_file() {
589 match Self::from_file(&path) {
590 Ok(config) if !config.peers.is_empty() => return Some((config, path)),
591 Ok(_) => {}
592 Err(err) => {
593 crate::logging::warn!(
594 "Failed to load bootstrap peers from {}: {err}",
595 path.display(),
596 );
597 }
598 }
599 }
600 }
601 None
602 }
603
604 fn search_paths() -> Vec<PathBuf> {
606 let mut paths = Vec::new();
607
608 if let Ok(env_path) = std::env::var(BOOTSTRAP_PEERS_ENV) {
610 paths.push(PathBuf::from(env_path));
611 }
612
613 if let Ok(exe) = std::env::current_exe() {
615 if let Some(exe_dir) = exe.parent() {
616 paths.push(exe_dir.join(BOOTSTRAP_PEERS_FILENAME));
617 }
618 }
619
620 if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "ant") {
622 paths.push(proj_dirs.config_dir().join(BOOTSTRAP_PEERS_FILENAME));
623 }
624
625 #[cfg(unix)]
627 {
628 paths.push(PathBuf::from("/etc/ant").join(BOOTSTRAP_PEERS_FILENAME));
629 }
630
631 paths
632 }
633}
634
635fn default_testnet_bootstrap() -> Vec<SocketAddr> {
641 vec![
642 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(165, 22, 4, 178), 12000)),
644 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(164, 92, 111, 156), 12000)),
646 ]
647}
648
649#[cfg(test)]
650#[allow(clippy::unwrap_used, clippy::expect_used)]
651mod tests {
652 use super::*;
653 use serial_test::serial;
654
655 #[test]
656 fn test_default_config_has_cache_capacity() {
657 let config = PaymentConfig::default();
658 assert!(config.cache_capacity > 0, "Cache capacity must be positive");
659 }
660
661 #[test]
662 fn test_default_evm_network() {
663 use crate::payment::EvmVerifierConfig;
664 let _config = EvmVerifierConfig::default();
665 }
667
668 #[test]
669 fn test_bootstrap_peers_parse_valid_toml() {
670 let toml_str = r#"
671 peers = [
672 "127.0.0.1:10000",
673 "192.168.1.1:10001",
674 ]
675 "#;
676 let config: BootstrapPeersConfig =
677 toml::from_str(toml_str).expect("valid TOML should parse");
678 assert_eq!(config.peers.len(), 2);
679 assert_eq!(config.peers[0].port(), 10000);
680 assert_eq!(config.peers[1].port(), 10001);
681 }
682
683 #[test]
684 fn test_bootstrap_peers_parse_empty_peers() {
685 let toml_str = r"peers = []";
686 let config: BootstrapPeersConfig =
687 toml::from_str(toml_str).expect("empty peers should parse");
688 assert!(config.peers.is_empty());
689 }
690
691 #[test]
692 fn test_bootstrap_peers_parse_missing_peers_field() {
693 let toml_str = "";
694 let config: BootstrapPeersConfig =
695 toml::from_str(toml_str).expect("missing field should use default");
696 assert!(config.peers.is_empty());
697 }
698
699 #[test]
700 fn test_bootstrap_peers_from_file() {
701 let dir = tempfile::tempdir().expect("create temp dir");
702 let path = dir.path().join("bootstrap_peers.toml");
703 std::fs::write(&path, r#"peers = ["10.0.0.1:10000", "10.0.0.2:10000"]"#)
704 .expect("write file");
705
706 let config = BootstrapPeersConfig::from_file(&path).expect("load from file");
707 assert_eq!(config.peers.len(), 2);
708 }
709
710 #[test]
711 fn test_bootstrap_peers_from_file_invalid_toml() {
712 let dir = tempfile::tempdir().expect("create temp dir");
713 let path = dir.path().join("bootstrap_peers.toml");
714 std::fs::write(&path, "not valid toml [[[").expect("write file");
715
716 assert!(BootstrapPeersConfig::from_file(&path).is_err());
717 }
718
719 #[test]
722 #[serial]
723 fn test_bootstrap_peers_discover_env_var() {
724 {
726 let dir = tempfile::tempdir().expect("create temp dir");
727 let path = dir.path().join("bootstrap_peers.toml");
728 std::fs::write(&path, r#"peers = ["10.0.0.1:10000"]"#).expect("write file");
729
730 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
731 let result = BootstrapPeersConfig::discover();
732 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
733
734 let (config, discovered_path) = result.expect("should discover from env var");
735 assert_eq!(config.peers.len(), 1);
736 assert_eq!(discovered_path, path);
737 }
738
739 {
741 let dir = tempfile::tempdir().expect("create temp dir");
742 let path = dir.path().join("bootstrap_peers.toml");
743 std::fs::write(&path, r"peers = []").expect("write file");
744
745 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
746 let result = BootstrapPeersConfig::discover();
747 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
748
749 assert!(result.is_none(), "empty peers file should be skipped");
750 }
751 }
752
753 #[test]
754 fn test_bootstrap_peers_search_paths_contains_exe_dir() {
755 let paths = BootstrapPeersConfig::search_paths();
756 assert!(
758 paths
759 .iter()
760 .any(|p| p.file_name().is_some_and(|f| f == BOOTSTRAP_PEERS_FILENAME)),
761 "search paths should include a candidate with the bootstrap peers filename"
762 );
763 }
764}