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 storage: StorageConfig,
122
123 #[serde(default)]
128 pub close_group_cache_dir: Option<PathBuf>,
129
130 #[serde(default = "default_max_message_size")]
139 pub max_message_size: usize,
140
141 #[serde(default = "default_log_level")]
143 pub log_level: String,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct UpgradeConfig {
149 #[serde(default)]
151 pub channel: UpgradeChannel,
152
153 #[serde(default = "default_check_interval")]
155 pub check_interval_hours: u64,
156
157 #[serde(default = "default_github_repo")]
159 pub github_repo: String,
160
161 #[serde(default = "default_staged_rollout_hours")]
169 pub staged_rollout_hours: u64,
170
171 #[serde(default)]
178 pub stop_on_upgrade: bool,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
189#[serde(rename_all = "kebab-case", tag = "type")]
190pub enum EvmNetworkConfig {
191 #[default]
193 ArbitrumOne,
194 ArbitrumSepolia,
196 Custom {
199 rpc_url: String,
201 payment_token_address: String,
203 payment_vault_address: String,
205 },
206}
207
208impl EvmNetworkConfig {
209 #[must_use]
212 pub fn into_evm_network(self) -> EvmNetwork {
213 match self {
214 Self::ArbitrumOne => EvmNetwork::ArbitrumOne,
215 Self::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest,
216 Self::Custom {
217 rpc_url,
218 payment_token_address,
219 payment_vault_address,
220 } => EvmNetwork::new_custom(&rpc_url, &payment_token_address, &payment_vault_address),
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct PaymentConfig {
232 #[serde(default = "default_cache_capacity")]
234 pub cache_capacity: usize,
235
236 #[serde(default)]
239 pub rewards_address: Option<String>,
240
241 #[serde(default)]
243 pub evm_network: EvmNetworkConfig,
244
245 #[serde(default = "default_metrics_port")]
248 pub metrics_port: u16,
249}
250
251impl Default for PaymentConfig {
252 fn default() -> Self {
253 Self {
254 cache_capacity: default_cache_capacity(),
255 rewards_address: None,
256 evm_network: EvmNetworkConfig::default(),
257 metrics_port: default_metrics_port(),
258 }
259 }
260}
261
262const fn default_metrics_port() -> u16 {
263 9100
264}
265
266const fn default_cache_capacity() -> usize {
267 100_000
268}
269
270impl Default for NodeConfig {
271 fn default() -> Self {
272 Self {
273 root_dir: default_root_dir(),
274 port: 0,
275 ipv4_only: false,
276 bootstrap: Vec::new(),
277 network_mode: NetworkMode::default(),
278 testnet: TestnetConfig::default(),
279 upgrade: UpgradeConfig::default(),
280 payment: PaymentConfig::default(),
281 storage: StorageConfig::default(),
282 close_group_cache_dir: None,
283 max_message_size: default_max_message_size(),
284 log_level: default_log_level(),
285 }
286 }
287}
288
289impl NodeConfig {
290 #[must_use]
296 pub fn testnet() -> Self {
297 Self {
298 network_mode: NetworkMode::Testnet,
299 testnet: TestnetConfig::default(),
300 bootstrap: default_testnet_bootstrap(),
301 ..Self::default()
302 }
303 }
304
305 #[must_use]
309 pub fn development() -> Self {
310 Self {
311 network_mode: NetworkMode::Development,
312 testnet: TestnetConfig {
313 max_per_ip: Some(usize::MAX),
314 max_per_subnet: Some(usize::MAX),
315 },
316 ..Self::default()
317 }
318 }
319
320 #[must_use]
322 pub fn is_relaxed(&self) -> bool {
323 !matches!(self.network_mode, NetworkMode::Production)
324 }
325
326 pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
332 let content = std::fs::read_to_string(path)?;
333 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
334 }
335
336 pub fn to_file(&self, path: &std::path::Path) -> crate::Result<()> {
342 let content =
343 toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
344 std::fs::write(path, content)?;
345 Ok(())
346 }
347}
348
349impl Default for UpgradeConfig {
350 fn default() -> Self {
351 Self {
352 channel: UpgradeChannel::default(),
353 check_interval_hours: default_check_interval(),
354 github_repo: default_github_repo(),
355 staged_rollout_hours: default_staged_rollout_hours(),
356 stop_on_upgrade: false,
357 }
358 }
359}
360
361fn default_github_repo() -> String {
362 "WithAutonomi/ant-node".to_string()
363}
364
365#[must_use]
367pub fn default_root_dir() -> PathBuf {
368 directories::ProjectDirs::from("", "", "ant").map_or_else(
369 || PathBuf::from(".ant"),
370 |dirs| dirs.data_dir().to_path_buf(),
371 )
372}
373
374#[must_use]
379pub fn default_nodes_dir() -> PathBuf {
380 default_root_dir().join(NODES_SUBDIR)
381}
382
383fn default_max_message_size() -> usize {
384 crate::replication::config::MAX_REPLICATION_MESSAGE_SIZE
388 .max(crate::ant_protocol::MAX_WIRE_MESSAGE_SIZE)
389}
390
391fn default_log_level() -> String {
392 "info".to_string()
393}
394
395const fn default_check_interval() -> u64 {
396 1 }
398
399const fn default_staged_rollout_hours() -> u64 {
400 24 }
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct StorageConfig {
415 #[serde(default = "default_storage_enabled")]
418 pub enabled: bool,
419
420 #[serde(default = "default_storage_verify_on_read")]
423 pub verify_on_read: bool,
424
425 #[serde(default)]
431 pub db_size_gb: usize,
432
433 #[serde(default = "default_disk_reserve_mb")]
438 pub disk_reserve_mb: u64,
439}
440
441impl Default for StorageConfig {
442 fn default() -> Self {
443 Self {
444 enabled: default_storage_enabled(),
445 verify_on_read: default_storage_verify_on_read(),
446 db_size_gb: 0,
447 disk_reserve_mb: default_disk_reserve_mb(),
448 }
449 }
450}
451
452const fn default_disk_reserve_mb() -> u64 {
454 500
455}
456
457const fn default_storage_enabled() -> bool {
458 true
459}
460
461const fn default_storage_verify_on_read() -> bool {
462 true
463}
464
465pub const BOOTSTRAP_PEERS_FILENAME: &str = "bootstrap_peers.toml";
471
472pub const BOOTSTRAP_PEERS_ENV: &str = "ANT_BOOTSTRAP_PEERS_PATH";
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct BootstrapPeersConfig {
480 #[serde(default)]
482 pub peers: Vec<SocketAddr>,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum BootstrapSource {
488 Cli,
490 ConfigFile,
492 AutoDiscovered(PathBuf),
494 None,
496}
497
498impl BootstrapPeersConfig {
499 pub fn from_file(path: &Path) -> crate::Result<Self> {
505 let content = std::fs::read_to_string(path)?;
506 toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
507 }
508
509 #[must_use]
521 pub fn discover() -> Option<(Self, PathBuf)> {
522 if let Ok(env_path) = std::env::var(BOOTSTRAP_PEERS_ENV) {
523 return Self::load_non_empty_candidate(PathBuf::from(env_path));
524 }
525
526 for path in Self::search_paths() {
527 if let Some(discovered) = Self::load_non_empty_candidate(path) {
528 return Some(discovered);
529 }
530 }
531
532 None
533 }
534
535 fn load_non_empty_candidate(path: PathBuf) -> Option<(Self, PathBuf)> {
536 if !path.is_file() {
537 return None;
538 }
539
540 match Self::from_file(&path) {
541 Ok(config) if !config.peers.is_empty() => Some((config, path)),
542 Ok(_) => None,
543 Err(err) => {
544 crate::logging::warn!(
545 "Failed to load bootstrap peers from {}: {err}",
546 path.display(),
547 );
548 None
549 }
550 }
551 }
552
553 fn search_paths() -> Vec<PathBuf> {
555 let mut paths = Vec::new();
556
557 if let Ok(env_path) = std::env::var(BOOTSTRAP_PEERS_ENV) {
559 paths.push(PathBuf::from(env_path));
560 }
561
562 if let Ok(exe) = std::env::current_exe() {
564 if let Some(exe_dir) = exe.parent() {
565 paths.push(exe_dir.join(BOOTSTRAP_PEERS_FILENAME));
566 }
567 }
568
569 if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "ant") {
571 paths.push(proj_dirs.config_dir().join(BOOTSTRAP_PEERS_FILENAME));
572 }
573
574 #[cfg(unix)]
576 {
577 paths.push(PathBuf::from("/etc/ant").join(BOOTSTRAP_PEERS_FILENAME));
578 }
579
580 paths
581 }
582}
583
584fn default_testnet_bootstrap() -> Vec<SocketAddr> {
590 vec![
591 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(165, 22, 4, 178), 12000)),
593 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(164, 92, 111, 156), 12000)),
595 ]
596}
597
598#[cfg(test)]
599#[allow(clippy::unwrap_used, clippy::expect_used)]
600mod tests {
601 use super::*;
602 use serial_test::serial;
603
604 #[test]
605 fn test_default_config_has_cache_capacity() {
606 let config = PaymentConfig::default();
607 assert!(config.cache_capacity > 0, "Cache capacity must be positive");
608 }
609
610 #[test]
611 fn test_default_evm_network() {
612 use crate::payment::EvmVerifierConfig;
613 let _config = EvmVerifierConfig::default();
614 }
616
617 #[test]
618 fn test_bootstrap_peers_parse_valid_toml() {
619 let toml_str = r#"
620 peers = [
621 "127.0.0.1:10000",
622 "192.168.1.1:10001",
623 ]
624 "#;
625 let config: BootstrapPeersConfig =
626 toml::from_str(toml_str).expect("valid TOML should parse");
627 assert_eq!(config.peers.len(), 2);
628 assert_eq!(config.peers[0].port(), 10000);
629 assert_eq!(config.peers[1].port(), 10001);
630 }
631
632 #[test]
633 fn test_bootstrap_peers_parse_empty_peers() {
634 let toml_str = r"peers = []";
635 let config: BootstrapPeersConfig =
636 toml::from_str(toml_str).expect("empty peers should parse");
637 assert!(config.peers.is_empty());
638 }
639
640 #[test]
641 fn test_bootstrap_peers_parse_missing_peers_field() {
642 let toml_str = "";
643 let config: BootstrapPeersConfig =
644 toml::from_str(toml_str).expect("missing field should use default");
645 assert!(config.peers.is_empty());
646 }
647
648 #[test]
649 fn test_bootstrap_peers_from_file() {
650 let dir = tempfile::tempdir().expect("create temp dir");
651 let path = dir.path().join("bootstrap_peers.toml");
652 std::fs::write(&path, r#"peers = ["10.0.0.1:10000", "10.0.0.2:10000"]"#)
653 .expect("write file");
654
655 let config = BootstrapPeersConfig::from_file(&path).expect("load from file");
656 assert_eq!(config.peers.len(), 2);
657 }
658
659 #[test]
660 fn test_bootstrap_peers_from_file_invalid_toml() {
661 let dir = tempfile::tempdir().expect("create temp dir");
662 let path = dir.path().join("bootstrap_peers.toml");
663 std::fs::write(&path, "not valid toml [[[").expect("write file");
664
665 assert!(BootstrapPeersConfig::from_file(&path).is_err());
666 }
667
668 #[test]
671 #[serial]
672 fn test_bootstrap_peers_discover_env_var() {
673 {
675 let dir = tempfile::tempdir().expect("create temp dir");
676 let path = dir.path().join("bootstrap_peers.toml");
677 std::fs::write(&path, r#"peers = ["10.0.0.1:10000"]"#).expect("write file");
678
679 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
680 let result = BootstrapPeersConfig::discover();
681 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
682
683 let (config, discovered_path) = result.expect("should discover from env var");
684 assert_eq!(config.peers.len(), 1);
685 assert_eq!(discovered_path, path);
686 }
687
688 {
690 let dir = tempfile::tempdir().expect("create temp dir");
691 let path = dir.path().join("bootstrap_peers.toml");
692 std::fs::write(&path, r"peers = []").expect("write file");
693
694 std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
695 let result = BootstrapPeersConfig::discover();
696 std::env::remove_var(BOOTSTRAP_PEERS_ENV);
697
698 assert!(result.is_none(), "empty peers file should be skipped");
699 }
700 }
701
702 #[test]
703 fn test_bootstrap_peers_search_paths_contains_exe_dir() {
704 let paths = BootstrapPeersConfig::search_paths();
705 assert!(
707 paths
708 .iter()
709 .any(|p| p.file_name().is_some_and(|f| f == BOOTSTRAP_PEERS_FILENAME)),
710 "search paths should include a candidate with the bootstrap peers filename"
711 );
712 }
713}