Skip to main content

saorsa_node/
config.rs

1//! Configuration for saorsa-node.
2
3use serde::{Deserialize, Serialize};
4use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
5use std::path::PathBuf;
6
7/// Filename for the persisted node identity keypair.
8pub const NODE_IDENTITY_FILENAME: &str = "node_identity.key";
9
10/// Subdirectory under the root dir that contains per-node data directories.
11pub const NODES_SUBDIR: &str = "nodes";
12
13/// IP version configuration.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum IpVersion {
17    /// IPv4 only.
18    Ipv4,
19    /// IPv6 only.
20    Ipv6,
21    /// Dual-stack (both IPv4 and IPv6).
22    #[default]
23    Dual,
24}
25
26/// Upgrade channel for auto-updates.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
28#[serde(rename_all = "lowercase")]
29pub enum UpgradeChannel {
30    /// Stable releases only.
31    #[default]
32    Stable,
33    /// Beta releases (includes stable).
34    Beta,
35}
36
37/// Network mode for different deployment scenarios.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum NetworkMode {
41    /// Production mode with full anti-Sybil protection.
42    #[default]
43    Production,
44    /// Testnet mode with relaxed diversity requirements.
45    /// Suitable for single-provider deployments (e.g., Digital Ocean).
46    Testnet,
47    /// Development mode with minimal restrictions.
48    /// Only use for local testing.
49    Development,
50}
51
52/// Testnet-specific configuration for relaxed anti-Sybil protection.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TestnetConfig {
55    /// Maximum nodes allowed per ASN.
56    /// Default: 5000 (compared to 20 in production).
57    #[serde(default = "default_testnet_max_per_asn")]
58    pub max_nodes_per_asn: usize,
59
60    /// Maximum nodes allowed per /64 subnet.
61    /// Default: 100 (compared to 1 in production).
62    #[serde(default = "default_testnet_max_per_64")]
63    pub max_nodes_per_64: usize,
64
65    /// Whether to enforce node age requirements.
66    /// Default: false (compared to true in production).
67    #[serde(default)]
68    pub enforce_age_requirements: bool,
69
70    /// Enable geographic diversity checks.
71    /// Default: false (compared to true in production).
72    #[serde(default)]
73    pub enable_geo_checks: bool,
74}
75
76impl Default for TestnetConfig {
77    fn default() -> Self {
78        Self {
79            max_nodes_per_asn: default_testnet_max_per_asn(),
80            max_nodes_per_64: default_testnet_max_per_64(),
81            enforce_age_requirements: false,
82            enable_geo_checks: false,
83        }
84    }
85}
86
87const fn default_testnet_max_per_asn() -> usize {
88    5000
89}
90
91const fn default_testnet_max_per_64() -> usize {
92    100
93}
94
95/// Node configuration.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct NodeConfig {
98    /// Root directory for node data.
99    #[serde(default = "default_root_dir")]
100    pub root_dir: PathBuf,
101
102    /// Listening port (0 for auto-select).
103    #[serde(default)]
104    pub port: u16,
105
106    /// IP version to use.
107    #[serde(default)]
108    pub ip_version: IpVersion,
109
110    /// Bootstrap peer addresses.
111    #[serde(default)]
112    pub bootstrap: Vec<SocketAddr>,
113
114    /// Network mode (production, testnet, or development).
115    #[serde(default)]
116    pub network_mode: NetworkMode,
117
118    /// Testnet-specific configuration.
119    /// Only used when `network_mode` is `Testnet`.
120    #[serde(default)]
121    pub testnet: TestnetConfig,
122
123    /// Upgrade configuration.
124    #[serde(default)]
125    pub upgrade: UpgradeConfig,
126
127    /// Payment verification configuration.
128    #[serde(default)]
129    pub payment: PaymentConfig,
130
131    /// Bootstrap cache configuration for persistent peer storage.
132    #[serde(default)]
133    pub bootstrap_cache: BootstrapCacheConfig,
134
135    /// Storage configuration for chunk persistence.
136    #[serde(default)]
137    pub storage: StorageConfig,
138
139    /// Maximum application-layer message size in bytes.
140    ///
141    /// Tunes the QUIC stream receive window and per-stream read buffer.
142    /// Default: [`MAX_WIRE_MESSAGE_SIZE`](crate::ant_protocol::MAX_WIRE_MESSAGE_SIZE)
143    /// (5 MiB — sufficient for 4 MiB data chunks plus serialization
144    /// envelope overhead).
145    #[serde(default = "default_max_message_size")]
146    pub max_message_size: usize,
147
148    /// Log level.
149    #[serde(default = "default_log_level")]
150    pub log_level: String,
151}
152
153/// Auto-upgrade configuration.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct UpgradeConfig {
156    /// Enable automatic upgrades.
157    #[serde(default)]
158    pub enabled: bool,
159
160    /// Release channel.
161    #[serde(default)]
162    pub channel: UpgradeChannel,
163
164    /// Check interval in hours.
165    #[serde(default = "default_check_interval")]
166    pub check_interval_hours: u64,
167
168    /// GitHub repository in "owner/repo" format for release monitoring.
169    #[serde(default = "default_github_repo")]
170    pub github_repo: String,
171
172    /// Staged rollout window in hours.
173    ///
174    /// When a new version is detected, each node waits a deterministic delay
175    /// based on its node ID before applying the upgrade. This prevents mass
176    /// restarts and ensures network stability during upgrades.
177    ///
178    /// Set to 0 to disable staged rollout (apply upgrades immediately).
179    #[serde(default = "default_staged_rollout_hours")]
180    pub staged_rollout_hours: u64,
181}
182
183/// EVM network for payment processing.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum EvmNetworkConfig {
187    /// Arbitrum One mainnet.
188    #[default]
189    ArbitrumOne,
190    /// Arbitrum Sepolia testnet.
191    ArbitrumSepolia,
192}
193
194/// Payment verification configuration.
195///
196/// All new data requires EVM payment on Arbitrum — there is no way to
197/// disable payment verification. The cache stores previously verified
198/// payments to avoid redundant on-chain lookups.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PaymentConfig {
201    /// Cache capacity for verified `XorNames`.
202    #[serde(default = "default_cache_capacity")]
203    pub cache_capacity: usize,
204
205    /// EVM wallet address for receiving payments (e.g., "0x...").
206    /// If not set, the node will not be able to receive payments.
207    #[serde(default)]
208    pub rewards_address: Option<String>,
209
210    /// EVM network for payment processing.
211    #[serde(default)]
212    pub evm_network: EvmNetworkConfig,
213
214    /// Metrics port for Prometheus scraping.
215    /// Set to 0 to disable metrics endpoint.
216    #[serde(default = "default_metrics_port")]
217    pub metrics_port: u16,
218}
219
220impl Default for PaymentConfig {
221    fn default() -> Self {
222        Self {
223            cache_capacity: default_cache_capacity(),
224            rewards_address: None,
225            evm_network: EvmNetworkConfig::default(),
226            metrics_port: default_metrics_port(),
227        }
228    }
229}
230
231const fn default_metrics_port() -> u16 {
232    9100
233}
234
235const fn default_cache_capacity() -> usize {
236    100_000
237}
238
239impl Default for NodeConfig {
240    fn default() -> Self {
241        Self {
242            root_dir: default_root_dir(),
243            port: 0,
244            ip_version: IpVersion::default(),
245            bootstrap: Vec::new(),
246            network_mode: NetworkMode::default(),
247            testnet: TestnetConfig::default(),
248            upgrade: UpgradeConfig::default(),
249            payment: PaymentConfig::default(),
250            bootstrap_cache: BootstrapCacheConfig::default(),
251            storage: StorageConfig::default(),
252            max_message_size: default_max_message_size(),
253            log_level: default_log_level(),
254        }
255    }
256}
257
258impl NodeConfig {
259    /// Create a testnet configuration preset.
260    ///
261    /// This is a convenience method for setting up a testnet node with
262    /// relaxed anti-Sybil protection, suitable for single-provider deployments.
263    /// Includes default bootstrap nodes for the Saorsa testnet.
264    #[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    /// Create a development configuration preset.
275    ///
276    /// This has minimal restrictions and is only suitable for local testing.
277    #[must_use]
278    pub fn development() -> Self {
279        Self {
280            network_mode: NetworkMode::Development,
281            testnet: TestnetConfig {
282                max_nodes_per_asn: usize::MAX,
283                max_nodes_per_64: usize::MAX,
284                enforce_age_requirements: false,
285                enable_geo_checks: false,
286            },
287            ..Self::default()
288        }
289    }
290
291    /// Check if this configuration is using relaxed security settings.
292    #[must_use]
293    pub fn is_relaxed(&self) -> bool {
294        !matches!(self.network_mode, NetworkMode::Production)
295    }
296
297    /// Load configuration from a TOML file.
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if the file cannot be read or parsed.
302    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    /// Save configuration to a TOML file.
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if the file cannot be written.
312    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            enabled: false,
324            channel: UpgradeChannel::default(),
325            check_interval_hours: default_check_interval(),
326            github_repo: default_github_repo(),
327            staged_rollout_hours: default_staged_rollout_hours(),
328        }
329    }
330}
331
332fn default_github_repo() -> String {
333    "dirvine/saorsa-node".to_string()
334}
335
336/// Default base directory for node data (platform data dir for "saorsa").
337#[must_use]
338pub fn default_root_dir() -> PathBuf {
339    directories::ProjectDirs::from("", "", "saorsa").map_or_else(
340        || PathBuf::from(".saorsa"),
341        |dirs| dirs.data_dir().to_path_buf(),
342    )
343}
344
345/// Default directory containing per-node data subdirectories.
346///
347/// Each node gets `{default_root_dir}/nodes/{peer_id}/` where `peer_id` is the
348/// full 64-character hex-encoded node ID.
349#[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::ant_protocol::MAX_WIRE_MESSAGE_SIZE
356}
357
358fn default_log_level() -> String {
359    "info".to_string()
360}
361
362const fn default_check_interval() -> u64 {
363    1 // 1 hour
364}
365
366const fn default_staged_rollout_hours() -> u64 {
367    1 // 1 hour window for staged rollout (testing)
368}
369
370// ============================================================================
371// Bootstrap Cache Configuration
372// ============================================================================
373
374/// Bootstrap cache configuration for persistent peer storage.
375///
376/// The bootstrap cache stores discovered peers across node restarts,
377/// ranking them by quality metrics (success rate, latency, recency).
378/// This reduces dependency on hardcoded bootstrap nodes and enables
379/// faster network reconnection after restarts.
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct BootstrapCacheConfig {
382    /// Enable persistent bootstrap cache.
383    /// Default: true
384    #[serde(default = "default_bootstrap_cache_enabled")]
385    pub enabled: bool,
386
387    /// Directory for cache files.
388    /// Default: `{root_dir}/bootstrap_cache/`
389    #[serde(default)]
390    pub cache_dir: Option<PathBuf>,
391
392    /// Maximum contacts to store in the cache.
393    /// Default: 10,000
394    #[serde(default = "default_bootstrap_max_contacts")]
395    pub max_contacts: usize,
396
397    /// Stale contact threshold in days.
398    /// Contacts older than this are removed during cleanup.
399    /// Default: 7 days
400    #[serde(default = "default_bootstrap_stale_days")]
401    pub stale_threshold_days: u64,
402}
403
404impl Default for BootstrapCacheConfig {
405    fn default() -> Self {
406        Self {
407            enabled: default_bootstrap_cache_enabled(),
408            cache_dir: None,
409            max_contacts: default_bootstrap_max_contacts(),
410            stale_threshold_days: default_bootstrap_stale_days(),
411        }
412    }
413}
414
415const fn default_bootstrap_cache_enabled() -> bool {
416    true
417}
418
419const fn default_bootstrap_max_contacts() -> usize {
420    10_000
421}
422
423const fn default_bootstrap_stale_days() -> u64 {
424    7
425}
426
427// ============================================================================
428// Storage Configuration
429// ============================================================================
430
431/// Storage configuration for chunk persistence.
432///
433/// Controls how chunks are stored, including:
434/// - Whether storage is enabled
435/// - Maximum chunks to store (for capacity management)
436/// - Content verification on read
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct StorageConfig {
439    /// Enable chunk storage.
440    /// Default: true
441    #[serde(default = "default_storage_enabled")]
442    pub enabled: bool,
443
444    /// Maximum number of chunks to store (0 = unlimited).
445    /// Default: 0 (unlimited)
446    #[serde(default)]
447    pub max_chunks: usize,
448
449    /// Verify content hash matches address on read.
450    /// Default: true
451    #[serde(default = "default_storage_verify_on_read")]
452    pub verify_on_read: bool,
453
454    /// Maximum LMDB database size in GiB (0 = use default of 32 GiB).
455    /// On Unix the mmap is a lazy reservation and costs nothing until pages
456    /// are faulted in.
457    #[serde(default)]
458    pub db_size_gb: usize,
459}
460
461impl Default for StorageConfig {
462    fn default() -> Self {
463        Self {
464            enabled: default_storage_enabled(),
465            max_chunks: 0,
466            verify_on_read: default_storage_verify_on_read(),
467            db_size_gb: 0,
468        }
469    }
470}
471
472const fn default_storage_enabled() -> bool {
473    true
474}
475
476const fn default_storage_verify_on_read() -> bool {
477    true
478}
479
480/// Default testnet bootstrap nodes.
481///
482/// These are well-known bootstrap nodes for the Saorsa testnet.
483/// - saorsa-bootstrap-1 (NYC): 165.22.4.178:12000
484/// - saorsa-bootstrap-2 (SFO): 164.92.111.156:12000
485fn default_testnet_bootstrap() -> Vec<SocketAddr> {
486    vec![
487        // saorsa-bootstrap-1 (Digital Ocean NYC1)
488        SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(165, 22, 4, 178), 12000)),
489        // saorsa-bootstrap-2 (Digital Ocean SFO3)
490        SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(164, 92, 111, 156), 12000)),
491    ]
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_default_config_has_cache_capacity() {
500        let config = PaymentConfig::default();
501        assert!(config.cache_capacity > 0, "Cache capacity must be positive");
502    }
503
504    #[test]
505    fn test_default_evm_network() {
506        use crate::payment::EvmVerifierConfig;
507        let _config = EvmVerifierConfig::default();
508        // EVM verification is always on — no enabled field
509    }
510}