Skip to main content

freenet/config/
mod.rs

1use std::{
2    collections::HashSet,
3    fs::{self, File},
4    future::Future,
5    io::{Read, Write},
6    net::{IpAddr, Ipv4Addr, SocketAddr},
7    path::{Path, PathBuf},
8    sync::{atomic::AtomicBool, Arc, LazyLock},
9    time::Duration,
10};
11
12use anyhow::Context;
13use directories::ProjectDirs;
14use either::Either;
15use serde::{Deserialize, Serialize};
16use tokio::runtime::Runtime;
17
18use crate::{
19    dev_tool::PeerId,
20    local_node::OperationMode,
21    tracing::tracer::get_log_dir,
22    transport::{CongestionControlAlgorithm, CongestionControlConfig, TransportKeypair},
23};
24
25mod secret;
26pub use secret::*;
27
28/// Default maximum number of connections for the peer.
29pub const DEFAULT_MAX_CONNECTIONS: usize = 20;
30/// Default minimum number of connections for the peer.
31pub const DEFAULT_MIN_CONNECTIONS: usize = 10;
32/// Default threshold for randomizing potential peers for new connections.
33///
34/// If the hops left for the operation is above or equal to this threshold
35/// (of the total DEFAULT_MAX_HOPS_TO_LIVE), then the next potential peer
36/// will be selected randomly. Otherwise the optimal peer will be selected
37/// by Freenet custom algorithms.
38pub const DEFAULT_RANDOM_PEER_CONN_THRESHOLD: usize = 7;
39/// Default maximum number of hops to live for any operation
40/// (if it applies, e.g. connect requests).
41pub const DEFAULT_MAX_HOPS_TO_LIVE: usize = 10;
42
43pub(crate) const OPERATION_TTL: Duration = Duration::from_secs(60);
44
45/// Current version of the crate.
46pub(crate) const PCK_VERSION: &str = env!("CARGO_PKG_VERSION");
47
48// Initialize the executor once.
49static ASYNC_RT: LazyLock<Option<Runtime>> = LazyLock::new(GlobalExecutor::initialize_async_rt);
50
51const DEFAULT_TRANSIENT_BUDGET: usize = 2048;
52const DEFAULT_TRANSIENT_TTL_SECS: u64 = 30;
53
54const QUALIFIER: &str = "";
55const ORGANIZATION: &str = "The Freenet Project Inc";
56const APPLICATION: &str = "Freenet";
57
58const FREENET_GATEWAYS_INDEX: &str = "https://freenet.org/keys/gateways.toml";
59
60#[derive(clap::Parser, Debug, Clone)]
61pub struct ConfigArgs {
62    /// Node operation mode. Default is network mode.
63    #[arg(value_enum, env = "MODE")]
64    pub mode: Option<OperationMode>,
65
66    #[command(flatten)]
67    pub ws_api: WebsocketApiArgs,
68
69    #[command(flatten)]
70    pub network_api: NetworkArgs,
71
72    #[command(flatten)]
73    pub secrets: SecretArgs,
74
75    #[arg(long, env = "LOG_LEVEL")]
76    pub log_level: Option<tracing::log::LevelFilter>,
77
78    #[command(flatten)]
79    pub config_paths: ConfigPathsArgs,
80
81    /// An arbitrary identifier for the node, mostly for debugging or testing purposes.
82    #[arg(long, hide = true)]
83    pub id: Option<String>,
84
85    /// Show the version of the application.
86    #[arg(long, short)]
87    pub version: bool,
88
89    /// Maximum number of threads for blocking operations (WASM execution, etc.).
90    /// Default: 2x CPU cores, clamped to 4-32.
91    #[arg(long, env = "MAX_BLOCKING_THREADS")]
92    pub max_blocking_threads: Option<usize>,
93
94    #[command(flatten)]
95    pub telemetry: TelemetryArgs,
96}
97
98impl Default for ConfigArgs {
99    fn default() -> Self {
100        Self {
101            mode: Some(OperationMode::Network),
102            network_api: NetworkArgs {
103                address: Some(default_listening_address()),
104                network_port: Some(default_network_api_port()),
105                public_address: None,
106                public_port: None,
107                is_gateway: false,
108                skip_load_from_network: true,
109                ignore_protocol_checking: false,
110                gateways: None,
111                location: None,
112                bandwidth_limit: Some(3_000_000), // 3 MB/s default for streaming transfers only
113                total_bandwidth_limit: None,
114                min_bandwidth_per_connection: None,
115                blocked_addresses: None,
116                transient_budget: Some(DEFAULT_TRANSIENT_BUDGET),
117                transient_ttl_secs: Some(DEFAULT_TRANSIENT_TTL_SECS),
118                min_connections: None,
119                max_connections: None,
120                streaming_threshold: None, // Default: 64KB (set in NetworkApiConfig)
121                ledbat_min_ssthresh: None, // Uses default from NetworkApiConfig
122                congestion_control: None,  // Default: fixedrate (set in NetworkApiConfig)
123                bbr_startup_rate: None,    // Uses default from BBR config
124            },
125            ws_api: WebsocketApiArgs {
126                address: Some(default_listening_address()),
127                ws_api_port: Some(default_ws_api_port()),
128                token_ttl_seconds: None,
129                token_cleanup_interval_seconds: None,
130            },
131            secrets: Default::default(),
132            log_level: Some(tracing::log::LevelFilter::Info),
133            config_paths: Default::default(),
134            id: None,
135            version: false,
136            max_blocking_threads: None,
137            telemetry: Default::default(),
138        }
139    }
140}
141
142impl ConfigArgs {
143    pub fn current_version(&self) -> &str {
144        PCK_VERSION
145    }
146
147    fn read_config(dir: &PathBuf) -> std::io::Result<Option<Config>> {
148        if !dir.exists() {
149            return Ok(None);
150        }
151        let mut read_dir = std::fs::read_dir(dir)?;
152        let config_args: Option<(String, String)> = read_dir.find_map(|e| {
153            if let Ok(e) = e {
154                if e.path().is_dir() {
155                    return None;
156                }
157                let filename = e.file_name().to_string_lossy().into_owned();
158                let ext = filename.rsplit('.').next().map(|s| s.to_owned());
159                if let Some(ext) = ext {
160                    if filename.starts_with("config") {
161                        match ext.as_str() {
162                            "toml" => {
163                                tracing::debug!(filename = %filename, "Found configuration file");
164                                return Some((filename, ext));
165                            }
166                            "json" => {
167                                return Some((filename, ext));
168                            }
169                            _ => {}
170                        }
171                    }
172                }
173            }
174
175            None
176        });
177
178        match config_args {
179            Some((filename, ext)) => {
180                let path = dir.join(filename).with_extension(&ext);
181                tracing::debug!(path = ?path, "Reading configuration file");
182                match ext.as_str() {
183                    "toml" => {
184                        let mut file = File::open(&path)?;
185                        let mut content = String::new();
186                        file.read_to_string(&mut content)?;
187                        let mut config = toml::from_str::<Config>(&content).map_err(|e| {
188                            std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
189                        })?;
190                        let secrets = Self::read_secrets(
191                            config.secrets.transport_keypair_path,
192                            config.secrets.nonce_path,
193                            config.secrets.cipher_path,
194                        )?;
195                        config.secrets = secrets;
196                        Ok(Some(config))
197                    }
198                    "json" => {
199                        let mut file = File::open(&path)?;
200                        let mut config = serde_json::from_reader::<_, Config>(&mut file)?;
201                        let secrets = Self::read_secrets(
202                            config.secrets.transport_keypair_path,
203                            config.secrets.nonce_path,
204                            config.secrets.cipher_path,
205                        )?;
206                        config.secrets = secrets;
207                        Ok(Some(config))
208                    }
209                    ext => Err(std::io::Error::new(
210                        std::io::ErrorKind::InvalidInput,
211                        format!("Invalid configuration file extension: {ext}"),
212                    )),
213                }
214            }
215            None => Ok(None),
216        }
217    }
218
219    /// Parse the command line arguments and return the configuration.
220    pub async fn build(mut self) -> anyhow::Result<Config> {
221        // Validate gateway configuration
222        self.network_api.validate()?;
223
224        let cfg = if let Some(path) = self.config_paths.config_dir.as_ref() {
225            if !path.exists() {
226                return Err(anyhow::Error::new(std::io::Error::new(
227                    std::io::ErrorKind::NotFound,
228                    "Configuration directory not found",
229                )));
230            }
231
232            Self::read_config(path)?
233        } else {
234            // find default application dir to see if there is a config file
235            let (config, data, is_temp_dir) = {
236                match ConfigPathsArgs::default_dirs(self.id.as_deref())? {
237                    Either::Left(defaults) => (
238                        defaults.config_local_dir().to_path_buf(),
239                        defaults.data_local_dir().to_path_buf(),
240                        false,
241                    ),
242                    Either::Right(dir) => (dir.clone(), dir, true),
243                }
244            };
245            self.config_paths.config_dir = Some(config.clone());
246            if self.config_paths.data_dir.is_none() {
247                self.config_paths.data_dir = Some(data);
248            }
249            // Skip reading config from temp directories (test scenarios) - they won't have config files
250            // and may have permission issues from previous runs
251            if is_temp_dir {
252                None
253            } else {
254                Self::read_config(&config)?.inspect(|_| {
255                    tracing::debug!("Found configuration file in default directory");
256                })
257            }
258        };
259
260        let should_persist = cfg.is_none();
261
262        // merge the configuration from the file with the command line arguments
263        if let Some(cfg) = cfg {
264            self.secrets.merge(cfg.secrets);
265            self.mode.get_or_insert(cfg.mode);
266            self.ws_api.address.get_or_insert(cfg.ws_api.address);
267            self.ws_api.ws_api_port.get_or_insert(cfg.ws_api.port);
268            self.ws_api
269                .token_ttl_seconds
270                .get_or_insert(cfg.ws_api.token_ttl_seconds);
271            self.ws_api
272                .token_cleanup_interval_seconds
273                .get_or_insert(cfg.ws_api.token_cleanup_interval_seconds);
274            self.network_api
275                .address
276                .get_or_insert(cfg.network_api.address);
277            self.network_api
278                .network_port
279                .get_or_insert(cfg.network_api.port);
280            if let Some(addr) = cfg.network_api.public_address {
281                self.network_api.public_address.get_or_insert(addr);
282            }
283            if let Some(port) = cfg.network_api.public_port {
284                self.network_api.public_port.get_or_insert(port);
285            }
286            if let Some(limit) = cfg.network_api.bandwidth_limit {
287                self.network_api.bandwidth_limit.get_or_insert(limit);
288            }
289            if let Some(addrs) = cfg.network_api.blocked_addresses {
290                self.network_api
291                    .blocked_addresses
292                    .get_or_insert_with(|| addrs.into_iter().collect());
293            }
294            self.network_api
295                .transient_budget
296                .get_or_insert(cfg.network_api.transient_budget);
297            self.network_api
298                .transient_ttl_secs
299                .get_or_insert(cfg.network_api.transient_ttl_secs);
300            self.network_api
301                .min_connections
302                .get_or_insert(cfg.network_api.min_connections);
303            self.network_api
304                .max_connections
305                .get_or_insert(cfg.network_api.max_connections);
306            if cfg.network_api.streaming_threshold != default_streaming_threshold() {
307                self.network_api
308                    .streaming_threshold
309                    .get_or_insert(cfg.network_api.streaming_threshold);
310            }
311            // Merge LEDBAT min_ssthresh: CLI args override config file, config file overrides default
312            if self.network_api.ledbat_min_ssthresh.is_none() {
313                self.network_api.ledbat_min_ssthresh = cfg.network_api.ledbat_min_ssthresh;
314            }
315            // Merge congestion control: CLI args override config file
316            if self.network_api.congestion_control.is_none()
317                && cfg.network_api.congestion_control != default_congestion_control()
318            {
319                self.network_api
320                    .congestion_control
321                    .get_or_insert(cfg.network_api.congestion_control);
322            }
323            if self.network_api.bbr_startup_rate.is_none() {
324                self.network_api.bbr_startup_rate = cfg.network_api.bbr_startup_rate;
325            }
326            self.log_level.get_or_insert(cfg.log_level);
327            self.config_paths.merge(cfg.config_paths.as_ref().clone());
328            // Merge telemetry config - CLI args override file config
329            // Note: enabled defaults to true via clap, so we only override
330            // if the config file explicitly sets it to false
331            if !cfg.telemetry.enabled {
332                self.telemetry.enabled = false;
333            }
334            if self.telemetry.endpoint.is_none() {
335                self.telemetry
336                    .endpoint
337                    .get_or_insert(cfg.telemetry.endpoint);
338            }
339        }
340
341        let mode = self.mode.unwrap_or(OperationMode::Network);
342        let config_paths = self.config_paths.build(self.id.as_deref())?;
343
344        let secrets = self.secrets.build(Some(&config_paths.secrets_dir(mode)))?;
345
346        let peer_id = self
347            .network_api
348            .public_address
349            .zip(self.network_api.public_port)
350            .map(|(addr, port)| {
351                PeerId::new(
352                    (addr, port).into(),
353                    secrets.transport_keypair.public().clone(),
354                )
355            });
356        let gateways_file = config_paths.config_dir.join("gateways.toml");
357
358        // In Local mode, skip all gateway loading since we don't connect to external peers
359        let remotely_loaded_gateways = if mode == OperationMode::Local {
360            Gateways::default()
361        } else if !self.network_api.skip_load_from_network {
362            load_gateways_from_index(FREENET_GATEWAYS_INDEX, &config_paths.secrets_dir)
363                .await
364                .inspect_err(|error| {
365                    tracing::error!(
366                        error = %error,
367                        index = FREENET_GATEWAYS_INDEX,
368                        "Failed to load gateways from index"
369                    );
370                })
371                .unwrap_or_default()
372        } else if let Some(gateways) = self.network_api.gateways {
373            let gateways = gateways
374                .into_iter()
375                .map(|cfg| {
376                    let cfg = serde_json::from_str::<InlineGwConfig>(&cfg)?;
377                    Ok::<_, anyhow::Error>(GatewayConfig {
378                        address: Address::HostAddress(cfg.address),
379                        public_key_path: cfg.public_key_path,
380                        location: cfg.location,
381                    })
382                })
383                .collect::<Result<Vec<_>, _>>()?;
384            Gateways { gateways }
385        } else {
386            Gateways::default()
387        };
388
389        // Decide which gateways to use based on whether we fetched from network
390        let gateways = if mode == OperationMode::Local {
391            // In Local mode, use empty gateways - no external connections
392            Gateways { gateways: vec![] }
393        } else if !self.network_api.skip_load_from_network
394            && !remotely_loaded_gateways.gateways.is_empty()
395        {
396            // When we successfully fetch gateways from the network, replace local ones entirely
397            // This ensures users always use the current active gateways
398            // TODO: This behavior will likely change once we release a stable version
399            tracing::info!(
400                gateway_count = remotely_loaded_gateways.gateways.len(),
401                "Replacing local gateways with gateways from remote index"
402            );
403
404            // Save the updated gateways to the local file for next time
405            if let Err(e) = remotely_loaded_gateways.save_to_file(&gateways_file) {
406                tracing::warn!(
407                    error = %e,
408                    file = ?gateways_file,
409                    "Failed to save updated gateways to file"
410                );
411            }
412
413            remotely_loaded_gateways
414        } else if self.network_api.skip_load_from_network && self.network_api.is_gateway {
415            // When skip_load_from_network is set for a gateway, run fully isolated.
416            // Don't connect to any other gateways - this enables isolated test networks
417            // where the test gateway doesn't mesh with production.
418            if remotely_loaded_gateways.gateways.is_empty() {
419                tracing::info!(
420                    "Gateway running in isolated mode (skip_load_from_network), not connecting to other gateways"
421                );
422                Gateways { gateways: vec![] }
423            } else {
424                // Inline gateways were provided via --gateways flag, use those
425                remotely_loaded_gateways
426            }
427        } else {
428            // When skip_load_from_network is set for a regular peer, use local gateways file
429            let mut gateways = match File::open(&*gateways_file) {
430                Ok(mut file) => {
431                    let mut content = String::new();
432                    file.read_to_string(&mut content)?;
433                    toml::from_str::<Gateways>(&content).map_err(|e| {
434                        std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
435                    })?
436                }
437                Err(err) => {
438                    if peer_id.is_none()
439                        && mode == OperationMode::Network
440                        && remotely_loaded_gateways.gateways.is_empty()
441                    {
442                        tracing::error!(
443                            file = ?gateways_file,
444                            error = %err,
445                            "Failed to read gateways file"
446                        );
447
448                        return Err(anyhow::Error::new(std::io::Error::new(
449                            std::io::ErrorKind::NotFound,
450                            "Cannot initialize node without gateways",
451                        )));
452                    }
453                    if remotely_loaded_gateways.gateways.is_empty() {
454                        tracing::warn!("No gateways file found, initializing disjoint gateway");
455                    }
456                    Gateways { gateways: vec![] }
457                }
458            };
459
460            // If we have remotely loaded gateways but skip_load_from_network was not set,
461            // it means the remote fetch failed but we got default/inline gateways
462            if !remotely_loaded_gateways.gateways.is_empty() {
463                gateways.merge_and_deduplicate(remotely_loaded_gateways);
464            }
465
466            gateways
467        };
468
469        let this = Config {
470            mode,
471            peer_id,
472            network_api: NetworkApiConfig {
473                address: self.network_api.address.unwrap_or_else(|| match mode {
474                    OperationMode::Local => default_local_address(),
475                    OperationMode::Network => default_listening_address(),
476                }),
477                port: self
478                    .network_api
479                    .network_port
480                    .unwrap_or_else(default_network_api_port),
481                public_address: self.network_api.public_address,
482                public_port: self.network_api.public_port,
483                ignore_protocol_version: self.network_api.ignore_protocol_checking,
484                bandwidth_limit: self.network_api.bandwidth_limit,
485                total_bandwidth_limit: self.network_api.total_bandwidth_limit,
486                min_bandwidth_per_connection: self.network_api.min_bandwidth_per_connection,
487                blocked_addresses: self
488                    .network_api
489                    .blocked_addresses
490                    .map(|addrs| addrs.into_iter().collect()),
491                transient_budget: self
492                    .network_api
493                    .transient_budget
494                    .unwrap_or(DEFAULT_TRANSIENT_BUDGET),
495                transient_ttl_secs: self
496                    .network_api
497                    .transient_ttl_secs
498                    .unwrap_or(DEFAULT_TRANSIENT_TTL_SECS),
499                min_connections: self
500                    .network_api
501                    .min_connections
502                    .unwrap_or(DEFAULT_MIN_CONNECTIONS),
503                max_connections: self
504                    .network_api
505                    .max_connections
506                    .unwrap_or(DEFAULT_MAX_CONNECTIONS),
507                streaming_threshold: self
508                    .network_api
509                    .streaming_threshold
510                    .unwrap_or_else(default_streaming_threshold),
511                ledbat_min_ssthresh: self
512                    .network_api
513                    .ledbat_min_ssthresh
514                    .or_else(default_ledbat_min_ssthresh),
515                congestion_control: self
516                    .network_api
517                    .congestion_control
518                    .clone()
519                    .unwrap_or_else(default_congestion_control),
520                bbr_startup_rate: self.network_api.bbr_startup_rate,
521            },
522            ws_api: WebsocketApiConfig {
523                address: {
524                    let addr = self.ws_api.address.unwrap_or_else(|| match mode {
525                        OperationMode::Local => default_local_address(),
526                        OperationMode::Network => default_listening_address(),
527                    });
528                    // In local mode, 0.0.0.0 is normalized to localhost
529                    match (mode, addr) {
530                        (OperationMode::Local, IpAddr::V4(ip)) if ip == Ipv4Addr::UNSPECIFIED => {
531                            default_local_address()
532                        }
533                        (OperationMode::Local, IpAddr::V6(ip)) if ip.is_unspecified() => {
534                            default_local_address()
535                        }
536                        _ => addr,
537                    }
538                },
539                port: self.ws_api.ws_api_port.unwrap_or(default_ws_api_port()),
540                token_ttl_seconds: self
541                    .ws_api
542                    .token_ttl_seconds
543                    .unwrap_or(default_token_ttl_seconds()),
544                token_cleanup_interval_seconds: self
545                    .ws_api
546                    .token_cleanup_interval_seconds
547                    .unwrap_or(default_token_cleanup_interval_seconds()),
548            },
549            secrets,
550            log_level: self.log_level.unwrap_or(tracing::log::LevelFilter::Info),
551            config_paths: Arc::new(config_paths),
552            gateways: gateways.gateways.clone(),
553            is_gateway: self.network_api.is_gateway,
554            location: self.network_api.location,
555            max_blocking_threads: self
556                .max_blocking_threads
557                .unwrap_or_else(default_max_blocking_threads),
558            telemetry: TelemetryConfig {
559                enabled: self.telemetry.enabled,
560                endpoint: self
561                    .telemetry
562                    .endpoint
563                    .unwrap_or_else(|| DEFAULT_TELEMETRY_ENDPOINT.to_string()),
564                transport_snapshot_interval_secs: self
565                    .telemetry
566                    .transport_snapshot_interval_secs
567                    .unwrap_or_else(default_transport_snapshot_interval_secs),
568                // Test environments are identified by the --id flag, which is used for
569                // simulated networks and integration tests. We disable telemetry in these
570                // environments to avoid flooding the collector with test data.
571                is_test_environment: self.id.is_some(),
572            },
573        };
574
575        fs::create_dir_all(this.config_dir())?;
576        // Only persist gateways when they were fetched from the remote index.
577        // When skip_load_from_network is set (local test networks), the gateways.toml
578        // is managed externally and should not be overwritten.
579        if !self.network_api.skip_load_from_network {
580            gateways.save_to_file(&gateways_file)?;
581        }
582
583        if should_persist {
584            let path = this.config_dir().join("config.toml");
585            tracing::info!(path = ?path, "Persisting configuration");
586            let mut file = File::create(path)?;
587            file.write_all(
588                toml::to_string(&this)
589                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
590                    .as_bytes(),
591            )?;
592        }
593
594        Ok(this)
595    }
596}
597
598mod serde_log_level_filter {
599    use serde::{Deserialize, Deserializer, Serializer};
600    use tracing::log::LevelFilter;
601
602    pub fn parse_log_level_str<'a, D>(level: &str) -> Result<LevelFilter, D::Error>
603    where
604        D: serde::Deserializer<'a>,
605    {
606        Ok(match level.trim() {
607            "off" | "Off" | "OFF" => LevelFilter::Off,
608            "error" | "Error" | "ERROR" => LevelFilter::Error,
609            "warn" | "Warn" | "WARN" => LevelFilter::Warn,
610            "info" | "Info" | "INFO" => LevelFilter::Info,
611            "debug" | "Debug" | "DEBUG" => LevelFilter::Debug,
612            "trace" | "Trace" | "TRACE" => LevelFilter::Trace,
613            s => return Err(serde::de::Error::custom(format!("unknown log level: {s}"))),
614        })
615    }
616
617    pub fn serialize<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
618    where
619        S: Serializer,
620    {
621        let level = match level {
622            LevelFilter::Off => "off",
623            LevelFilter::Error => "error",
624            LevelFilter::Warn => "warn",
625            LevelFilter::Info => "info",
626            LevelFilter::Debug => "debug",
627            LevelFilter::Trace => "trace",
628        };
629        serializer.serialize_str(level)
630    }
631
632    pub fn deserialize<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
633    where
634        D: Deserializer<'de>,
635    {
636        let level = String::deserialize(deserializer)?;
637        parse_log_level_str::<D>(level.as_str())
638    }
639}
640
641#[derive(Debug, Serialize, Deserialize, Clone)]
642pub struct Config {
643    /// Node operation mode.
644    pub mode: OperationMode,
645    #[serde(flatten)]
646    pub network_api: NetworkApiConfig,
647    #[serde(flatten)]
648    pub ws_api: WebsocketApiConfig,
649    #[serde(flatten)]
650    pub secrets: Secrets,
651    #[serde(with = "serde_log_level_filter")]
652    pub log_level: tracing::log::LevelFilter,
653    #[serde(flatten)]
654    config_paths: Arc<ConfigPaths>,
655    #[serde(skip)]
656    pub(crate) peer_id: Option<PeerId>,
657    #[serde(skip)]
658    pub(crate) gateways: Vec<GatewayConfig>,
659    pub(crate) is_gateway: bool,
660    pub(crate) location: Option<f64>,
661    /// Maximum number of threads for blocking operations (WASM execution, etc.).
662    #[serde(default = "default_max_blocking_threads")]
663    pub max_blocking_threads: usize,
664    /// Telemetry configuration
665    #[serde(flatten)]
666    pub telemetry: TelemetryConfig,
667}
668
669/// Default max blocking threads: 2x CPU cores, clamped to 4-32.
670fn default_max_blocking_threads() -> usize {
671    std::thread::available_parallelism()
672        .map(|n| (n.get() * 2).clamp(4, 32))
673        .unwrap_or(8)
674}
675
676impl Config {
677    pub fn transport_keypair(&self) -> &TransportKeypair {
678        self.secrets.transport_keypair()
679    }
680
681    pub fn paths(&self) -> Arc<ConfigPaths> {
682        self.config_paths.clone()
683    }
684}
685
686#[derive(clap::Parser, Debug, Default, Clone, Serialize, Deserialize)]
687pub struct NetworkArgs {
688    /// Address to bind to for the network event listener, default is 0.0.0.0
689    #[arg(
690        name = "network_address",
691        long = "network-address",
692        env = "NETWORK_ADDRESS"
693    )]
694    #[serde(rename = "network-address", skip_serializing_if = "Option::is_none")]
695    pub address: Option<IpAddr>,
696
697    /// Port to bind for the network event listener, default is 31337
698    #[arg(long, env = "NETWORK_PORT")]
699    #[serde(rename = "network-port", skip_serializing_if = "Option::is_none")]
700    pub network_port: Option<u16>,
701
702    /// Public address for the network. Required for gateways.
703    #[arg(long = "public-network-address", env = "PUBLIC_NETWORK_ADDRESS")]
704    #[serde(
705        rename = "public-network-address",
706        skip_serializing_if = "Option::is_none"
707    )]
708    pub public_address: Option<IpAddr>,
709
710    /// Public port for the network. Required for gateways.
711    #[arg(long = "public-network-port", env = "PUBLIC_NETWORK_PORT")]
712    #[serde(
713        rename = "public-network-port",
714        skip_serializing_if = "Option::is_none"
715    )]
716    pub public_port: Option<u16>,
717
718    /// Whether the node is a gateway or not.
719    /// If the node is a gateway, it will be able to accept connections from other nodes.
720    #[arg(long)]
721    pub is_gateway: bool,
722
723    /// Skips loading gateway configurations from the network and merging it with existing one.
724    #[arg(long)]
725    pub skip_load_from_network: bool,
726
727    /// Optional list of gateways to connect to in network mode. Used for testing purposes.
728    #[arg(long, hide = true)]
729    pub gateways: Option<Vec<String>>,
730
731    /// Optional location of the node, this is to be able to deterministically set locations for gateways for testing purposes.
732    #[arg(long, hide = true, env = "LOCATION")]
733    pub location: Option<f64>,
734
735    /// Ignores protocol version failures, continuing to run the node if there is a mismatch with the gateway.
736    #[arg(long)]
737    pub ignore_protocol_checking: bool,
738
739    /// Bandwidth limit for large streaming data transfers (in bytes per second).
740    /// NOTE: This only applies to the send_stream mechanism for large data transfers.
741    /// The general packet rate limiter is currently disabled due to reliability issues.
742    /// Default: 3 MB/s (3,000,000 bytes/second)
743    #[arg(long)]
744    pub bandwidth_limit: Option<usize>,
745
746    /// Total bandwidth limit across ALL connections (in bytes per second).
747    /// When set, individual connection rates are computed as: total / active_connections.
748    /// This overrides the per-connection bandwidth_limit.
749    #[arg(long)]
750    #[serde(
751        rename = "total-bandwidth-limit",
752        skip_serializing_if = "Option::is_none"
753    )]
754    pub total_bandwidth_limit: Option<usize>,
755
756    /// Minimum bandwidth per connection when using total_bandwidth_limit (bytes/sec).
757    /// Prevents connection starvation when many connections are active.
758    /// Default: 1 MB/s (1,000,000 bytes/second)
759    #[arg(long)]
760    #[serde(
761        rename = "min-bandwidth-per-connection",
762        skip_serializing_if = "Option::is_none"
763    )]
764    pub min_bandwidth_per_connection: Option<usize>,
765
766    /// List of IP:port addresses to refuse connections to/from.
767    #[arg(long, num_args = 0..)]
768    pub blocked_addresses: Option<Vec<SocketAddr>>,
769
770    /// Maximum number of concurrent transient connections accepted by a gateway.
771    #[arg(long, env = "TRANSIENT_BUDGET")]
772    #[serde(rename = "transient-budget", skip_serializing_if = "Option::is_none")]
773    pub transient_budget: Option<usize>,
774
775    /// Time (in seconds) before an unpromoted transient connection is dropped.
776    #[arg(long, env = "TRANSIENT_TTL_SECS")]
777    #[serde(rename = "transient-ttl-secs", skip_serializing_if = "Option::is_none")]
778    pub transient_ttl_secs: Option<u64>,
779
780    /// Minimum desired connections for the ring topology. Defaults to 10.
781    #[arg(long = "min-number-of-connections", env = "MIN_NUMBER_OF_CONNECTIONS")]
782    #[serde(
783        rename = "min-number-of-connections",
784        skip_serializing_if = "Option::is_none"
785    )]
786    pub min_connections: Option<usize>,
787
788    /// Maximum allowed connections for the ring topology. Defaults to 20.
789    #[arg(long = "max-number-of-connections", env = "MAX_NUMBER_OF_CONNECTIONS")]
790    #[serde(
791        rename = "max-number-of-connections",
792        skip_serializing_if = "Option::is_none"
793    )]
794    pub max_connections: Option<usize>,
795
796    /// Threshold in bytes above which streaming transport is used.
797    /// Default: 65536 (64KB)
798    #[arg(long, env = "STREAMING_THRESHOLD")]
799    #[serde(
800        rename = "streaming-threshold",
801        skip_serializing_if = "Option::is_none"
802    )]
803    pub streaming_threshold: Option<usize>,
804
805    /// Minimum ssthresh floor for LEDBAT timeout recovery (bytes).
806    ///
807    /// On high-latency paths (>100ms RTT), repeated timeouts can cause ssthresh
808    /// to collapse to ~5KB, severely limiting throughput recovery.
809    /// Setting a higher floor prevents this "ssthresh death spiral".
810    ///
811    /// Recommended values by network type:
812    /// - LAN (<10ms RTT): None (use default)
813    /// - Regional (10-50ms): None (use default)
814    /// - Continental (50-100ms): 51200 (50KB)
815    /// - Intercontinental (100-200ms): 102400-512000 (100KB-500KB)
816    /// - Satellite (500ms+): 524288-2097152 (500KB-2MB)
817    ///
818    /// Default: None (uses spec-compliant 2*min_cwnd ≈ 5.7KB floor)
819    #[arg(long, env = "LEDBAT_MIN_SSTHRESH")]
820    #[serde(
821        rename = "ledbat-min-ssthresh",
822        skip_serializing_if = "Option::is_none"
823    )]
824    pub ledbat_min_ssthresh: Option<usize>,
825
826    /// Congestion control algorithm for transport connections.
827    ///
828    /// Available algorithms:
829    /// - `fixedrate` (default): Fixed-rate transmission at 10 Mbps per connection, ignores network feedback
830    /// - `bbr`: BBR (Bottleneck Bandwidth and RTT) - model-based, tolerates packet loss
831    /// - `ledbat`: LEDBAT++ - delay-based, yields to foreground traffic
832    ///
833    /// Default: `fixedrate` (most stable for production)
834    #[arg(long, env = "FREENET_CONGESTION_CONTROL")]
835    #[serde(rename = "congestion-control", skip_serializing_if = "Option::is_none")]
836    pub congestion_control: Option<String>,
837
838    /// BBR startup minimum pacing rate (bytes/sec).
839    ///
840    /// Only used when congestion_control is set to "bbr".
841    /// Lower values are safer for virtualized/constrained network environments (like CI).
842    ///
843    /// Default: 25 MB/s (25_000_000 bytes/sec)
844    #[arg(long, env = "FREENET_BBR_STARTUP_RATE")]
845    #[serde(rename = "bbr-startup-rate", skip_serializing_if = "Option::is_none")]
846    pub bbr_startup_rate: Option<u64>,
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
850pub struct InlineGwConfig {
851    /// Address of the gateway.
852    pub address: SocketAddr,
853
854    /// Path to the public key of the gateway in PEM format.
855    #[serde(rename = "public_key")]
856    pub public_key_path: PathBuf,
857
858    /// Optional location of the gateway. Necessary for deterministic testing.
859    pub location: Option<f64>,
860}
861
862impl NetworkArgs {
863    pub(crate) fn validate(&self) -> anyhow::Result<()> {
864        if self.is_gateway {
865            // For gateways, require both public address and port
866            if self.public_address.is_none() {
867                return Err(anyhow::anyhow!(
868                    "Gateway nodes must specify a public network address"
869                ));
870            }
871            if self.public_port.is_none() && self.network_port.is_none() {
872                return Err(anyhow::anyhow!("Gateway nodes must specify a network port"));
873            }
874        }
875        Ok(())
876    }
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize)]
880pub struct NetworkApiConfig {
881    /// Address to listen to locally
882    #[serde(default = "default_listening_address", rename = "network-address")]
883    pub address: IpAddr,
884
885    /// Port to expose api on
886    #[serde(default = "default_network_api_port", rename = "network-port")]
887    pub port: u16,
888
889    /// Public external address for the network, mandatory for gateways.
890    #[serde(
891        rename = "public_network_address",
892        skip_serializing_if = "Option::is_none"
893    )]
894    pub public_address: Option<IpAddr>,
895
896    /// Public external port for the network, mandatory for gateways.
897    #[serde(rename = "public_port", skip_serializing_if = "Option::is_none")]
898    pub public_port: Option<u16>,
899
900    /// Whether to ignore protocol version compatibility routine while initiating connections.
901    #[serde(skip)]
902    pub ignore_protocol_version: bool,
903
904    /// Bandwidth limit per connection for data transfers (in bytes per second).
905    /// NOTE: This applies to each connection independently - N connections may use N * bandwidth_limit total.
906    /// Each connection uses LEDBAT congestion control to yield to foreground traffic.
907    /// Default: 10 MB/s (10,000,000 bytes/second)
908    ///
909    /// If `total_bandwidth_limit` is set, this field is ignored and per-connection rates
910    /// are derived from: `total_bandwidth_limit / active_connections`.
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub bandwidth_limit: Option<usize>,
913
914    /// Total bandwidth limit across ALL connections (in bytes per second).
915    /// When set, individual connection rates are computed as: `total / active_connections`.
916    /// This overrides the per-connection `bandwidth_limit`.
917    ///
918    /// Example: With 50 MB/s total and 5 connections, each gets 10 MB/s.
919    /// Default: None (use per-connection `bandwidth_limit` instead)
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub total_bandwidth_limit: Option<usize>,
922
923    /// Minimum bandwidth per connection when using `total_bandwidth_limit` (bytes/sec).
924    /// Prevents connection starvation when many connections are active.
925    ///
926    /// If `total / N < min`, each connection gets `min` (exceeding total is possible).
927    /// Default: 1 MB/s (1,000,000 bytes/second)
928    #[serde(skip_serializing_if = "Option::is_none")]
929    pub min_bandwidth_per_connection: Option<usize>,
930
931    /// List of IP:port addresses to refuse connections to/from.
932    #[serde(skip_serializing_if = "Option::is_none")]
933    pub blocked_addresses: Option<HashSet<SocketAddr>>,
934
935    /// Maximum number of concurrent transient connections accepted by a gateway.
936    #[serde(default = "default_transient_budget", rename = "transient-budget")]
937    pub transient_budget: usize,
938
939    /// Time (in seconds) before an unpromoted transient connection is dropped.
940    #[serde(default = "default_transient_ttl_secs", rename = "transient-ttl-secs")]
941    pub transient_ttl_secs: u64,
942
943    /// Minimum desired connections for the ring topology.
944    #[serde(
945        default = "default_min_connections",
946        rename = "min-number-of-connections"
947    )]
948    pub min_connections: usize,
949
950    /// Maximum allowed connections for the ring topology.
951    #[serde(
952        default = "default_max_connections",
953        rename = "max-number-of-connections"
954    )]
955    pub max_connections: usize,
956
957    /// Threshold in bytes above which streaming transport is used.
958    /// Default: 65536 (64KB)
959    #[serde(
960        default = "default_streaming_threshold",
961        rename = "streaming-threshold"
962    )]
963    pub streaming_threshold: usize,
964
965    /// Minimum ssthresh floor for LEDBAT timeout recovery (bytes).
966    ///
967    /// On high-latency paths (>100ms RTT), repeated timeouts can cause ssthresh
968    /// to collapse to ~5KB, severely limiting throughput recovery.
969    /// Setting a higher floor prevents this "ssthresh death spiral".
970    ///
971    /// Default: 102400 (100KB) - suitable for intercontinental connections.
972    /// Set to None for LAN-only deployments.
973    #[serde(
974        default = "default_ledbat_min_ssthresh",
975        rename = "ledbat-min-ssthresh",
976        skip_serializing_if = "Option::is_none"
977    )]
978    pub ledbat_min_ssthresh: Option<usize>,
979
980    /// Congestion control algorithm for transport connections.
981    ///
982    /// Available algorithms:
983    /// - `fixedrate` (default): Fixed-rate transmission at 10 Mbps per connection
984    /// - `bbr`: BBR (Bottleneck Bandwidth and RTT)
985    /// - `ledbat`: LEDBAT++ (Low Extra Delay Background Transport)
986    #[serde(default = "default_congestion_control", rename = "congestion-control")]
987    pub congestion_control: String,
988
989    /// BBR startup minimum pacing rate (bytes/sec).
990    ///
991    /// Only used when congestion_control is "bbr".
992    #[serde(
993        default = "default_bbr_startup_rate",
994        rename = "bbr-startup-rate",
995        skip_serializing_if = "Option::is_none"
996    )]
997    pub bbr_startup_rate: Option<u64>,
998}
999
1000impl NetworkApiConfig {
1001    /// Build a `CongestionControlConfig` from the current network API configuration.
1002    ///
1003    /// This parses the `congestion_control` string to determine the algorithm
1004    /// and applies any algorithm-specific settings like `bbr_startup_rate`.
1005    pub fn build_congestion_config(&self) -> CongestionControlConfig {
1006        let algo = match self.congestion_control.to_lowercase().as_str() {
1007            "bbr" => CongestionControlAlgorithm::Bbr,
1008            "ledbat" => CongestionControlAlgorithm::Ledbat,
1009            _ => CongestionControlAlgorithm::FixedRate, // Default for production
1010        };
1011
1012        let mut config = CongestionControlConfig::new(algo);
1013
1014        // Apply BBR-specific settings
1015        if algo == CongestionControlAlgorithm::Bbr {
1016            if let Some(rate) = self.bbr_startup_rate {
1017                tracing::debug!("Using custom BBR startup pacing rate: {} bytes/sec", rate);
1018                config = config.with_startup_min_pacing_rate(rate);
1019            }
1020        }
1021
1022        config
1023    }
1024}
1025
1026mod port_allocation;
1027use port_allocation::find_available_port;
1028
1029pub fn default_network_api_port() -> u16 {
1030    find_available_port().unwrap_or(31337) // Fallback to 31337 if we can't find a random port
1031}
1032
1033fn default_transient_budget() -> usize {
1034    DEFAULT_TRANSIENT_BUDGET
1035}
1036
1037fn default_transient_ttl_secs() -> u64 {
1038    DEFAULT_TRANSIENT_TTL_SECS
1039}
1040
1041fn default_min_connections() -> usize {
1042    DEFAULT_MIN_CONNECTIONS
1043}
1044
1045fn default_max_connections() -> usize {
1046    DEFAULT_MAX_CONNECTIONS
1047}
1048
1049/// Default streaming threshold: 64KB
1050fn default_streaming_threshold() -> usize {
1051    64 * 1024
1052}
1053
1054/// Default minimum ssthresh for LEDBAT timeout recovery.
1055///
1056/// Returns `Some(100KB)` - suitable for intercontinental connections where
1057/// repeated timeouts could otherwise cause ssthresh to collapse to ~5KB.
1058///
1059/// See: docs/architecture/transport/configuration/bandwidth-configuration.md
1060fn default_ledbat_min_ssthresh() -> Option<usize> {
1061    Some(100 * 1024) // 100KB floor
1062}
1063
1064/// Default congestion control algorithm.
1065///
1066/// Returns "fixedrate" - the most stable option for production.
1067fn default_congestion_control() -> String {
1068    "fixedrate".to_string()
1069}
1070
1071/// Default BBR startup pacing rate.
1072///
1073/// Returns None to use the BBR default (25 MB/s).
1074fn default_bbr_startup_rate() -> Option<u64> {
1075    None
1076}
1077
1078#[derive(clap::Parser, Debug, Default, Copy, Clone, Serialize, Deserialize)]
1079pub struct WebsocketApiArgs {
1080    /// Address to bind to for the websocket API, default is 0.0.0.0
1081    #[arg(
1082        name = "ws_api_address",
1083        long = "ws-api-address",
1084        env = "WS_API_ADDRESS"
1085    )]
1086    #[serde(rename = "ws-api-address", skip_serializing_if = "Option::is_none")]
1087    pub address: Option<IpAddr>,
1088
1089    /// Port to expose the websocket on, default is 7509
1090    #[arg(long, env = "WS_API_PORT")]
1091    #[serde(rename = "ws-api-port", skip_serializing_if = "Option::is_none")]
1092    pub ws_api_port: Option<u16>,
1093
1094    /// Token time-to-live in seconds (default is 86400 = 24 hours)
1095    #[arg(long, env = "TOKEN_TTL_SECONDS")]
1096    #[serde(rename = "token-ttl-seconds", skip_serializing_if = "Option::is_none")]
1097    pub token_ttl_seconds: Option<u64>,
1098
1099    /// Token cleanup interval in seconds (default is 300 = 5 minutes)
1100    #[arg(long, env = "TOKEN_CLEANUP_INTERVAL_SECONDS")]
1101    #[serde(
1102        rename = "token-cleanup-interval-seconds",
1103        skip_serializing_if = "Option::is_none"
1104    )]
1105    pub token_cleanup_interval_seconds: Option<u64>,
1106}
1107
1108/// Default telemetry endpoint (nova.locut.us OTLP collector).
1109/// Using domain name for resilience to IP changes.
1110pub const DEFAULT_TELEMETRY_ENDPOINT: &str = "http://nova.locut.us:4318";
1111
1112#[derive(clap::Parser, Debug, Clone, Serialize, Deserialize)]
1113pub struct TelemetryArgs {
1114    /// Enable telemetry reporting to help improve Freenet (default: true during alpha).
1115    /// Telemetry includes operation timing and network topology data, but never contract content.
1116    #[arg(
1117        long = "telemetry-enabled",
1118        env = "FREENET_TELEMETRY_ENABLED",
1119        default_value = "true"
1120    )]
1121    #[serde(rename = "telemetry-enabled", default = "default_telemetry_enabled")]
1122    pub enabled: bool,
1123
1124    /// Telemetry endpoint URL (OTLP/HTTP format)
1125    #[arg(long = "telemetry-endpoint", env = "FREENET_TELEMETRY_ENDPOINT")]
1126    #[serde(rename = "telemetry-endpoint", skip_serializing_if = "Option::is_none")]
1127    pub endpoint: Option<String>,
1128
1129    /// Interval in seconds for emitting transport layer metric snapshots.
1130    /// Set to 0 to disable transport snapshots. Default: 30 seconds.
1131    #[arg(
1132        long = "transport-snapshot-interval-secs",
1133        env = "FREENET_TRANSPORT_SNAPSHOT_INTERVAL_SECS"
1134    )]
1135    #[serde(
1136        rename = "transport-snapshot-interval-secs",
1137        skip_serializing_if = "Option::is_none"
1138    )]
1139    pub transport_snapshot_interval_secs: Option<u64>,
1140}
1141
1142impl Default for TelemetryArgs {
1143    fn default() -> Self {
1144        Self {
1145            enabled: true,
1146            endpoint: None,
1147            transport_snapshot_interval_secs: None,
1148        }
1149    }
1150}
1151
1152fn default_telemetry_enabled() -> bool {
1153    true
1154}
1155
1156#[derive(Debug, Clone, Serialize, Deserialize)]
1157pub struct TelemetryConfig {
1158    /// Whether telemetry reporting is enabled
1159    #[serde(default = "default_telemetry_enabled", rename = "telemetry-enabled")]
1160    pub enabled: bool,
1161
1162    /// Telemetry endpoint URL
1163    #[serde(default = "default_telemetry_endpoint", rename = "telemetry-endpoint")]
1164    pub endpoint: String,
1165
1166    /// Interval in seconds for emitting transport layer metric snapshots.
1167    /// Set to 0 to disable transport snapshots.
1168    /// Default: 30 seconds.
1169    #[serde(
1170        default = "default_transport_snapshot_interval_secs",
1171        rename = "transport-snapshot-interval-secs"
1172    )]
1173    pub transport_snapshot_interval_secs: u64,
1174
1175    /// Whether this is a test environment (detected via --id flag).
1176    /// When true, telemetry is disabled to avoid flooding the collector with test data.
1177    #[serde(skip)]
1178    pub is_test_environment: bool,
1179}
1180
1181fn default_transport_snapshot_interval_secs() -> u64 {
1182    30
1183}
1184
1185fn default_telemetry_endpoint() -> String {
1186    DEFAULT_TELEMETRY_ENDPOINT.to_string()
1187}
1188
1189impl Default for TelemetryConfig {
1190    fn default() -> Self {
1191        Self {
1192            enabled: true,
1193            endpoint: DEFAULT_TELEMETRY_ENDPOINT.to_string(),
1194            transport_snapshot_interval_secs: default_transport_snapshot_interval_secs(),
1195            is_test_environment: false,
1196        }
1197    }
1198}
1199
1200#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
1201pub struct WebsocketApiConfig {
1202    /// Address to bind to
1203    #[serde(default = "default_listening_address", rename = "ws-api-address")]
1204    pub address: IpAddr,
1205
1206    /// Port to expose api on
1207    #[serde(default = "default_ws_api_port", rename = "ws-api-port")]
1208    pub port: u16,
1209
1210    /// Token time-to-live in seconds
1211    #[serde(default = "default_token_ttl_seconds", rename = "token-ttl-seconds")]
1212    pub token_ttl_seconds: u64,
1213
1214    /// Token cleanup interval in seconds
1215    #[serde(
1216        default = "default_token_cleanup_interval_seconds",
1217        rename = "token-cleanup-interval-seconds"
1218    )]
1219    pub token_cleanup_interval_seconds: u64,
1220}
1221
1222#[inline]
1223const fn default_token_ttl_seconds() -> u64 {
1224    86400 // 24 hours
1225}
1226
1227#[inline]
1228const fn default_token_cleanup_interval_seconds() -> u64 {
1229    300 // 5 minutes
1230}
1231
1232impl From<SocketAddr> for WebsocketApiConfig {
1233    fn from(addr: SocketAddr) -> Self {
1234        Self {
1235            address: addr.ip(),
1236            port: addr.port(),
1237            token_ttl_seconds: default_token_ttl_seconds(),
1238            token_cleanup_interval_seconds: default_token_cleanup_interval_seconds(),
1239        }
1240    }
1241}
1242
1243impl Default for WebsocketApiConfig {
1244    #[inline]
1245    fn default() -> Self {
1246        Self {
1247            address: default_listening_address(),
1248            port: default_ws_api_port(),
1249            token_ttl_seconds: default_token_ttl_seconds(),
1250            token_cleanup_interval_seconds: default_token_cleanup_interval_seconds(),
1251        }
1252    }
1253}
1254
1255#[inline]
1256const fn default_listening_address() -> IpAddr {
1257    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1258}
1259
1260#[inline]
1261const fn default_local_address() -> IpAddr {
1262    IpAddr::V4(Ipv4Addr::LOCALHOST)
1263}
1264
1265#[inline]
1266const fn default_ws_api_port() -> u16 {
1267    7509
1268}
1269
1270#[derive(clap::Parser, Default, Debug, Clone, Serialize, Deserialize)]
1271pub struct ConfigPathsArgs {
1272    /// The configuration directory.
1273    #[arg(long, default_value = None, env = "CONFIG_DIR")]
1274    pub config_dir: Option<PathBuf>,
1275    /// The data directory.
1276    #[arg(long, default_value = None, env = "DATA_DIR")]
1277    pub data_dir: Option<PathBuf>,
1278    /// The log directory.
1279    #[arg(long, default_value = None, env = "LOG_DIR")]
1280    pub log_dir: Option<PathBuf>,
1281}
1282
1283impl ConfigPathsArgs {
1284    fn merge(&mut self, other: ConfigPaths) {
1285        self.config_dir.get_or_insert(other.config_dir);
1286        self.data_dir.get_or_insert(other.data_dir);
1287        self.log_dir = self.log_dir.take().or(other.log_dir);
1288    }
1289
1290    fn default_dirs(id: Option<&str>) -> std::io::Result<Either<ProjectDirs, PathBuf>> {
1291        // if id is set, most likely we are running tests or in simulated mode
1292        let default_dir: Either<_, _> = if cfg!(any(test, debug_assertions)) || id.is_some() {
1293            let base_name = if let Some(id) = id {
1294                format!("freenet-{id}")
1295            } else {
1296                "freenet".into()
1297            };
1298            let temp_path = std::env::temp_dir().join(&base_name);
1299
1300            // Clean up stale temp directories from previous test runs that may have
1301            // different permissions (common on shared CI runners). If we can't remove
1302            // the stale directory (permission denied, in use, etc.), use a unique
1303            // fallback path with process ID to avoid conflicts.
1304            if temp_path.exists() && fs::remove_dir_all(&temp_path).is_err() {
1305                let unique_path =
1306                    std::env::temp_dir().join(format!("{}-{}", base_name, std::process::id()));
1307                // Clean up any stale unique path too (unlikely but possible)
1308                let _cleanup = fs::remove_dir_all(&unique_path);
1309                return Ok(Either::Right(unique_path));
1310            }
1311            Either::Right(temp_path)
1312        } else {
1313            Either::Left(
1314                ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
1315                    .ok_or(std::io::ErrorKind::NotFound)?,
1316            )
1317        };
1318        Ok(default_dir)
1319    }
1320
1321    pub fn build(self, id: Option<&str>) -> std::io::Result<ConfigPaths> {
1322        let app_data_dir = self
1323            .data_dir
1324            .map(Ok::<_, std::io::Error>)
1325            .unwrap_or_else(|| {
1326                let default_dirs = Self::default_dirs(id)?;
1327                let Either::Left(defaults) = default_dirs else {
1328                    unreachable!("default_dirs should return Left if data_dir is None and id is not set for temp dir")
1329                };
1330                Ok(defaults.data_dir().to_path_buf())
1331            })?;
1332        let contracts_dir = app_data_dir.join("contracts");
1333        let delegates_dir = app_data_dir.join("delegates");
1334        let secrets_dir = app_data_dir.join("secrets");
1335        let db_dir = app_data_dir.join("db");
1336
1337        if !contracts_dir.exists() {
1338            fs::create_dir_all(&contracts_dir)?;
1339            fs::create_dir_all(contracts_dir.join("local"))?;
1340        }
1341
1342        if !delegates_dir.exists() {
1343            fs::create_dir_all(&delegates_dir)?;
1344            fs::create_dir_all(delegates_dir.join("local"))?;
1345        }
1346
1347        if !secrets_dir.exists() {
1348            fs::create_dir_all(&secrets_dir)?;
1349            fs::create_dir_all(secrets_dir.join("local"))?;
1350        }
1351
1352        if !db_dir.exists() {
1353            fs::create_dir_all(&db_dir)?;
1354            fs::create_dir_all(db_dir.join("local"))?;
1355        }
1356
1357        let event_log = app_data_dir.join("_EVENT_LOG");
1358        if !event_log.exists() {
1359            fs::write(&event_log, [])?;
1360            let mut local_file = event_log.clone();
1361            local_file.set_file_name("_EVENT_LOG_LOCAL");
1362            fs::write(local_file, [])?;
1363        }
1364
1365        let config_dir = self
1366            .config_dir
1367            .map(Ok::<_, std::io::Error>)
1368            .unwrap_or_else(|| {
1369                let default_dirs = Self::default_dirs(id)?;
1370                let Either::Left(defaults) = default_dirs else {
1371                    unreachable!("default_dirs should return Left if config_dir is None and id is not set for temp dir")
1372                };
1373                Ok(defaults.config_dir().to_path_buf())
1374            })?;
1375
1376        let log_dir = self.log_dir.or_else(get_log_dir);
1377
1378        Ok(ConfigPaths {
1379            config_dir,
1380            data_dir: app_data_dir,
1381            contracts_dir,
1382            delegates_dir,
1383            secrets_dir,
1384            db_dir,
1385            event_log,
1386            log_dir,
1387        })
1388    }
1389}
1390
1391#[derive(Debug, Clone, Serialize, Deserialize)]
1392pub struct ConfigPaths {
1393    contracts_dir: PathBuf,
1394    delegates_dir: PathBuf,
1395    secrets_dir: PathBuf,
1396    db_dir: PathBuf,
1397    event_log: PathBuf,
1398    data_dir: PathBuf,
1399    config_dir: PathBuf,
1400    #[serde(default = "get_log_dir")]
1401    log_dir: Option<PathBuf>,
1402}
1403
1404impl ConfigPaths {
1405    pub fn db_dir(&self, mode: OperationMode) -> PathBuf {
1406        match mode {
1407            OperationMode::Local => self.db_dir.join("local"),
1408            OperationMode::Network => self.db_dir.to_owned(),
1409        }
1410    }
1411
1412    pub fn with_db_dir(mut self, db_dir: PathBuf) -> Self {
1413        self.db_dir = db_dir;
1414        self
1415    }
1416
1417    pub fn contracts_dir(&self, mode: OperationMode) -> PathBuf {
1418        match mode {
1419            OperationMode::Local => self.contracts_dir.join("local"),
1420            OperationMode::Network => self.contracts_dir.to_owned(),
1421        }
1422    }
1423
1424    pub fn with_contract_dir(mut self, contracts_dir: PathBuf) -> Self {
1425        self.contracts_dir = contracts_dir;
1426        self
1427    }
1428
1429    pub fn delegates_dir(&self, mode: OperationMode) -> PathBuf {
1430        match mode {
1431            OperationMode::Local => self.delegates_dir.join("local"),
1432            OperationMode::Network => self.delegates_dir.to_owned(),
1433        }
1434    }
1435
1436    pub fn with_delegates_dir(mut self, delegates_dir: PathBuf) -> Self {
1437        self.delegates_dir = delegates_dir;
1438        self
1439    }
1440
1441    pub fn config_dir(&self) -> PathBuf {
1442        self.config_dir.clone()
1443    }
1444
1445    pub fn secrets_dir(&self, mode: OperationMode) -> PathBuf {
1446        match mode {
1447            OperationMode::Local => self.secrets_dir.join("local"),
1448            OperationMode::Network => self.secrets_dir.to_owned(),
1449        }
1450    }
1451
1452    pub fn with_secrets_dir(mut self, secrets_dir: PathBuf) -> Self {
1453        self.secrets_dir = secrets_dir;
1454        self
1455    }
1456
1457    pub fn event_log(&self, mode: OperationMode) -> PathBuf {
1458        match mode {
1459            OperationMode::Local => {
1460                let mut local_file = self.event_log.clone();
1461                local_file.set_file_name("_EVENT_LOG_LOCAL");
1462                local_file
1463            }
1464            OperationMode::Network => self.event_log.to_owned(),
1465        }
1466    }
1467
1468    pub fn log_dir(&self) -> Option<&Path> {
1469        self.log_dir.as_deref()
1470    }
1471
1472    pub fn with_event_log(mut self, event_log: PathBuf) -> Self {
1473        self.event_log = event_log;
1474        self
1475    }
1476
1477    pub fn iter(&self) -> ConfigPathsIter<'_> {
1478        ConfigPathsIter {
1479            curr: 0,
1480            config_paths: self,
1481        }
1482    }
1483
1484    fn path_by_index(&self, index: usize) -> (bool, &PathBuf) {
1485        match index {
1486            0 => (true, &self.contracts_dir),
1487            1 => (true, &self.delegates_dir),
1488            2 => (true, &self.secrets_dir),
1489            3 => (true, &self.db_dir),
1490            4 => (true, &self.data_dir),
1491            5 => (false, &self.event_log),
1492            6 => (true, &self.config_dir),
1493            _ => panic!("invalid path index"),
1494        }
1495    }
1496
1497    const MAX_PATH_INDEX: usize = 6;
1498}
1499
1500pub struct ConfigPathsIter<'a> {
1501    curr: usize,
1502    config_paths: &'a ConfigPaths,
1503}
1504
1505impl<'a> Iterator for ConfigPathsIter<'a> {
1506    /// The first is whether this path is a directory or a file.
1507    type Item = (bool, &'a PathBuf);
1508
1509    fn next(&mut self) -> Option<Self::Item> {
1510        if self.curr > ConfigPaths::MAX_PATH_INDEX {
1511            None
1512        } else {
1513            let path = self.config_paths.path_by_index(self.curr);
1514            self.curr += 1;
1515            Some(path)
1516        }
1517    }
1518
1519    fn size_hint(&self) -> (usize, Option<usize>) {
1520        (0, Some(ConfigPaths::MAX_PATH_INDEX))
1521    }
1522}
1523
1524impl core::iter::FusedIterator for ConfigPathsIter<'_> {}
1525
1526impl Config {
1527    pub fn db_dir(&self) -> PathBuf {
1528        self.config_paths.db_dir(self.mode)
1529    }
1530
1531    pub fn contracts_dir(&self) -> PathBuf {
1532        self.config_paths.contracts_dir(self.mode)
1533    }
1534
1535    pub fn delegates_dir(&self) -> PathBuf {
1536        self.config_paths.delegates_dir(self.mode)
1537    }
1538
1539    pub fn secrets_dir(&self) -> PathBuf {
1540        self.config_paths.secrets_dir(self.mode)
1541    }
1542
1543    pub fn event_log(&self) -> PathBuf {
1544        self.config_paths.event_log(self.mode)
1545    }
1546
1547    pub fn config_dir(&self) -> PathBuf {
1548        self.config_paths.config_dir()
1549    }
1550}
1551
1552#[derive(Debug, Serialize, Deserialize, Default)]
1553struct Gateways {
1554    pub gateways: Vec<GatewayConfig>,
1555}
1556
1557impl Gateways {
1558    pub fn merge_and_deduplicate(&mut self, other: Gateways) {
1559        let mut existing_gateways: HashSet<_> = self.gateways.drain(..).collect();
1560        for gateway in other.gateways {
1561            existing_gateways.insert(gateway);
1562        }
1563        self.gateways = existing_gateways.into_iter().collect();
1564    }
1565
1566    pub fn save_to_file(&self, path: &Path) -> anyhow::Result<()> {
1567        // Ensure parent directory exists (fixes Windows first-run where config dir may not exist)
1568        if let Some(parent) = path.parent() {
1569            fs::create_dir_all(parent)?;
1570        }
1571        let content = toml::to_string(self)?;
1572        fs::write(path, content)?;
1573        Ok(())
1574    }
1575}
1576
1577#[derive(Debug, Serialize, Deserialize, Clone)]
1578pub struct GatewayConfig {
1579    /// Address of the gateway. It can be either a hostname or an IP address and port.
1580    pub address: Address,
1581
1582    /// Path to the public key of the gateway in PEM format.
1583    #[serde(rename = "public_key")]
1584    pub public_key_path: PathBuf,
1585
1586    /// Optional location of the gateway.
1587    #[serde(skip_serializing_if = "Option::is_none")]
1588    pub location: Option<f64>,
1589}
1590
1591impl PartialEq for GatewayConfig {
1592    fn eq(&self, other: &Self) -> bool {
1593        self.address == other.address
1594    }
1595}
1596
1597impl Eq for GatewayConfig {}
1598
1599impl std::hash::Hash for GatewayConfig {
1600    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1601        self.address.hash(state);
1602    }
1603}
1604
1605#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
1606pub enum Address {
1607    #[serde(rename = "hostname")]
1608    Hostname(String),
1609    #[serde(rename = "host_address")]
1610    HostAddress(SocketAddr),
1611}
1612
1613/// Global async executor abstraction for spawning tasks.
1614///
1615/// This abstraction allows swapping the underlying executor for deterministic
1616/// simulation testing. In production, it delegates to tokio. For deterministic
1617/// simulation, use Turmoil which provides deterministic task scheduling.
1618///
1619/// # Usage
1620/// ```ignore
1621/// use freenet::config::GlobalExecutor;
1622/// GlobalExecutor::spawn(async { /* task */ });
1623/// ```
1624pub struct GlobalExecutor;
1625
1626impl GlobalExecutor {
1627    /// Returns the runtime handle if it was initialized or none if it was already
1628    /// running on the background.
1629    pub(crate) fn initialize_async_rt() -> Option<Runtime> {
1630        if tokio::runtime::Handle::try_current().is_ok() {
1631            tracing::debug!(target: "freenet::diagnostics::thread_explosion", "GlobalExecutor: runtime exists");
1632            None
1633        } else {
1634            tracing::warn!(target: "freenet::diagnostics::thread_explosion", "GlobalExecutor: Creating fallback runtime");
1635            let mut builder = tokio::runtime::Builder::new_multi_thread();
1636            builder.enable_all().thread_name("freenet-node");
1637            if cfg!(debug_assertions) {
1638                builder.worker_threads(2).max_blocking_threads(2);
1639            }
1640            Some(builder.build().expect("failed to build tokio runtime"))
1641        }
1642    }
1643
1644    #[inline]
1645    pub fn spawn<R: Send + 'static>(
1646        f: impl Future<Output = R> + Send + 'static,
1647    ) -> tokio::task::JoinHandle<R> {
1648        if let Ok(handle) = tokio::runtime::Handle::try_current() {
1649            handle.spawn(f)
1650        } else if let Some(rt) = &*ASYNC_RT {
1651            tracing::warn!(target: "freenet::diagnostics::thread_explosion", "GlobalExecutor::spawn using fallback");
1652            rt.spawn(f)
1653        } else {
1654            unreachable!("ASYNC_RT should be initialized if Handle::try_current fails")
1655        }
1656    }
1657}
1658
1659// =============================================================================
1660// GlobalRng - Deterministic RNG abstraction for simulation testing
1661// =============================================================================
1662
1663use rand::rngs::SmallRng;
1664use rand::{Rng, RngCore, SeedableRng};
1665
1666static THREAD_INDEX_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1667
1668std::thread_local! {
1669    static THREAD_RNG: std::cell::RefCell<Option<SmallRng>> = const { std::cell::RefCell::new(None) };
1670    static THREAD_INDEX: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
1671    static THREAD_SEED: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
1672}
1673
1674/// Global RNG abstraction for deterministic simulation testing.
1675///
1676/// In production mode (no seed set), this delegates to the system RNG.
1677/// In simulation mode (seed set via `set_seed`), this uses a deterministic
1678/// seeded RNG that produces reproducible results.
1679///
1680/// # Test Isolation
1681///
1682/// For test isolation, prefer `scoped_seed()` or `SeedGuard` over `set_seed()`:
1683///
1684/// ```ignore
1685/// use freenet::config::GlobalRng;
1686///
1687/// // Option 1: Scoped seed (recommended for tests)
1688/// // Automatically clears seed when closure returns
1689/// GlobalRng::scoped_seed(0xDEADBEEF, || {
1690///     let value = GlobalRng::random_range(0..100); // Deterministic
1691/// });
1692/// // Seed automatically cleared here
1693///
1694/// // Option 2: RAII guard (for complex control flow)
1695/// {
1696///     let _guard = GlobalRng::seed_guard(0xDEADBEEF);
1697///     let value = GlobalRng::random_range(0..100); // Deterministic
1698/// } // Seed automatically cleared when guard drops
1699///
1700/// // Option 3: Manual set/clear (use with caution)
1701/// GlobalRng::set_seed(0xDEADBEEF);
1702/// // ... operations ...
1703/// GlobalRng::clear_seed(); // Don't forget this!
1704/// ```
1705pub struct GlobalRng;
1706
1707/// RAII guard that clears the GlobalRng seed when dropped.
1708///
1709/// This ensures test isolation by automatically restoring the RNG to
1710/// production mode (system randomness) when the guard goes out of scope,
1711/// even if the test panics.
1712///
1713/// # Example
1714/// ```ignore
1715/// use freenet::config::GlobalRng;
1716///
1717/// #[test]
1718/// fn my_deterministic_test() {
1719///     let _guard = GlobalRng::seed_guard(12345);
1720///     // All RNG operations are now deterministic
1721///     assert_eq!(GlobalRng::random_range(0..100), 42); // Always same value
1722/// } // Guard drops here, seed is cleared
1723/// ```
1724pub struct SeedGuard {
1725    // Private field prevents external construction
1726    _private: (),
1727}
1728
1729impl Drop for SeedGuard {
1730    fn drop(&mut self) {
1731        GlobalRng::clear_seed();
1732    }
1733}
1734
1735impl GlobalRng {
1736    /// Sets the thread-local seed for deterministic RNG.
1737    ///
1738    /// **Warning:** For test isolation, prefer `scoped_seed()` or `seed_guard()`
1739    /// which automatically clean up the seed state.
1740    ///
1741    /// Call this at test/simulation startup for reproducibility.
1742    /// Must call `clear_seed()` when done to avoid affecting other tests.
1743    ///
1744    /// This is purely thread-local — parallel tests on different threads are fully isolated.
1745    pub fn set_seed(seed: u64) {
1746        THREAD_SEED.with(|s| s.set(Some(seed)));
1747        THREAD_RNG.with(|rng| {
1748            *rng.borrow_mut() = None;
1749        });
1750        // Pin thread index to 0 so the derived RNG seed is deterministic
1751        // regardless of which OS thread runs this test (see #2733).
1752        THREAD_INDEX.with(|idx| idx.set(Some(0)));
1753    }
1754
1755    /// Clears the simulation seed, reverting to system RNG.
1756    pub fn clear_seed() {
1757        THREAD_SEED.with(|s| s.set(None));
1758        THREAD_RNG.with(|rng| {
1759            *rng.borrow_mut() = None;
1760        });
1761        THREAD_INDEX.with(|idx| idx.set(None));
1762    }
1763
1764    /// Returns the deterministic thread index for the current thread.
1765    ///
1766    /// Each thread gets a unique index from the global `THREAD_INDEX_COUNTER`.
1767    /// This is used by thread-local ID counters to compute non-overlapping offset blocks.
1768    pub fn thread_index() -> u64 {
1769        THREAD_INDEX.with(|c| match c.get() {
1770            Some(idx) => idx,
1771            None => {
1772                let idx = THREAD_INDEX_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1773                c.set(Some(idx));
1774                idx
1775            }
1776        })
1777    }
1778
1779    /// Returns true if a simulation seed is set for the current thread.
1780    pub fn is_seeded() -> bool {
1781        THREAD_SEED.with(|s| s.get()).is_some()
1782    }
1783
1784    /// Creates a RAII guard that sets the seed and clears it on drop.
1785    ///
1786    /// This is the recommended way to use deterministic RNG in tests,
1787    /// as it guarantees cleanup even if the test panics.
1788    ///
1789    /// # Example
1790    /// ```ignore
1791    /// let _guard = GlobalRng::seed_guard(12345);
1792    /// // All operations here use seeded RNG
1793    /// let x = GlobalRng::random_range(0..100);
1794    /// // Guard drops at end of scope, seed cleared automatically
1795    /// ```
1796    pub fn seed_guard(seed: u64) -> SeedGuard {
1797        Self::set_seed(seed);
1798        SeedGuard { _private: () }
1799    }
1800
1801    /// Executes a closure with a seeded RNG, then clears the seed.
1802    ///
1803    /// This is the safest way to use deterministic RNG in tests:
1804    /// - The seed is automatically cleared when the closure returns
1805    /// - Works correctly even if the closure panics (uses catch_unwind internally)
1806    ///
1807    /// # Example
1808    /// ```ignore
1809    /// let result = GlobalRng::scoped_seed(12345, || {
1810    ///     // Deterministic operations
1811    ///     GlobalRng::random_range(0..100)
1812    /// });
1813    /// // Seed is cleared here, regardless of success or panic
1814    /// ```
1815    pub fn scoped_seed<F, R>(seed: u64, f: F) -> R
1816    where
1817        F: FnOnce() -> R,
1818    {
1819        let _guard = Self::seed_guard(seed);
1820        f()
1821    }
1822
1823    /// Executes a closure with access to the RNG.
1824    /// Uses seeded RNG if set via `set_seed()`, otherwise system RNG.
1825    #[inline]
1826    pub fn with_rng<F, R>(f: F) -> R
1827    where
1828        F: FnOnce(&mut dyn RngCore) -> R,
1829    {
1830        // Thread-local seed only — no global fallback. This ensures parallel tests
1831        // on different threads are fully isolated.
1832        let seed = THREAD_SEED.with(|s| s.get());
1833
1834        if let Some(seed) = seed {
1835            // Simulation mode: use thread-local seeded RNG
1836            THREAD_RNG.with(|rng_cell| {
1837                let mut rng_ref = rng_cell.borrow_mut();
1838                if rng_ref.is_none() {
1839                    let thread_seed =
1840                        seed.wrapping_add(Self::thread_index().wrapping_mul(0x9E3779B97F4A7C15));
1841                    *rng_ref = Some(SmallRng::seed_from_u64(thread_seed));
1842                }
1843                f(rng_ref.as_mut().unwrap())
1844            })
1845        } else {
1846            // Production mode: use system RNG
1847            f(&mut rand::rng())
1848        }
1849    }
1850
1851    /// Generate a random value in the given range.
1852    #[inline]
1853    pub fn random_range<T, R>(range: R) -> T
1854    where
1855        T: rand::distr::uniform::SampleUniform,
1856        R: rand::distr::uniform::SampleRange<T>,
1857    {
1858        Self::with_rng(|rng| rng.random_range(range))
1859    }
1860
1861    /// Generate a random boolean with the given probability of being true.
1862    #[inline]
1863    pub fn random_bool(probability: f64) -> bool {
1864        Self::with_rng(|rng| rng.random_bool(probability))
1865    }
1866
1867    /// Choose a random element from a slice.
1868    #[inline]
1869    pub fn choose<T>(slice: &[T]) -> Option<&T> {
1870        if slice.is_empty() {
1871            None
1872        } else {
1873            let idx = Self::random_range(0..slice.len());
1874            Some(&slice[idx])
1875        }
1876    }
1877
1878    /// Shuffle a slice in place.
1879    #[inline]
1880    pub fn shuffle<T>(slice: &mut [T]) {
1881        Self::with_rng(|rng| {
1882            use rand::seq::SliceRandom;
1883            slice.shuffle(rng);
1884        })
1885    }
1886
1887    /// Fill a byte slice with random data.
1888    #[inline]
1889    pub fn fill_bytes(dest: &mut [u8]) {
1890        Self::with_rng(|rng| rng.fill_bytes(dest))
1891    }
1892
1893    /// Generate a random u64.
1894    #[inline]
1895    pub fn random_u64() -> u64 {
1896        Self::with_rng(|rng| rng.random())
1897    }
1898
1899    /// Generate a random u32.
1900    #[inline]
1901    pub fn random_u32() -> u32 {
1902        Self::with_rng(|rng| rng.random())
1903    }
1904}
1905
1906// =============================================================================
1907// Global Simulation Time
1908// =============================================================================
1909
1910// Thread-local simulation time: allows parallel simulation tests without interference.
1911std::thread_local! {
1912    static SIMULATION_TIME_MS: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
1913    static SIMULATION_TIME_COUNTER: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1914}
1915
1916/// Global simulation time configuration for deterministic testing.
1917///
1918/// In production mode (no simulation time set), ULID generation uses real system time.
1919/// In simulation mode, a configurable base time is used, ensuring reproducible transaction IDs.
1920///
1921/// # Usage
1922///
1923/// ```ignore
1924/// use freenet::config::GlobalSimulationTime;
1925///
1926/// // Set simulation time to a known epoch
1927/// GlobalSimulationTime::set_time_ms(1704067200000); // 2024-01-01 00:00:00 UTC
1928///
1929/// // All ULIDs generated after this use simulation time
1930/// let tx = Transaction::new::<SomeOp>();
1931///
1932/// // Clear when done
1933/// GlobalSimulationTime::clear_time();
1934/// ```
1935pub struct GlobalSimulationTime;
1936
1937impl GlobalSimulationTime {
1938    /// Sets the simulation time base in milliseconds since Unix epoch (thread-local).
1939    ///
1940    /// All subsequent ULID generations on this thread will use this time (with auto-increment).
1941    pub fn set_time_ms(time_ms: u64) {
1942        SIMULATION_TIME_MS.with(|t| t.set(Some(time_ms)));
1943        SIMULATION_TIME_COUNTER.with(|c| c.set(0));
1944    }
1945
1946    /// Clears the simulation time, reverting to system time (thread-local).
1947    pub fn clear_time() {
1948        SIMULATION_TIME_MS.with(|t| t.set(None));
1949        SIMULATION_TIME_COUNTER.with(|c| c.set(0));
1950    }
1951
1952    /// Returns the current time in milliseconds for ULID generation.
1953    ///
1954    /// If simulation time is set, returns simulation time + counter increment.
1955    /// Otherwise, returns real system time.
1956    pub fn current_time_ms() -> u64 {
1957        SIMULATION_TIME_MS.with(|t| {
1958            if let Some(base_time) = t.get() {
1959                let counter = SIMULATION_TIME_COUNTER.with(|c| {
1960                    let val = c.get();
1961                    c.set(val + 1);
1962                    val
1963                });
1964                base_time.saturating_add(counter)
1965            } else {
1966                use std::time::{SystemTime, UNIX_EPOCH};
1967                SystemTime::now()
1968                    .duration_since(UNIX_EPOCH)
1969                    .expect("system time before unix epoch")
1970                    .as_millis() as u64
1971            }
1972        })
1973    }
1974
1975    /// Returns the current time in milliseconds WITHOUT incrementing the counter.
1976    ///
1977    /// Use this for read-only time checks like elapsed time calculations.
1978    /// For ULID generation, use `current_time_ms()` which ensures uniqueness.
1979    pub fn read_time_ms() -> u64 {
1980        SIMULATION_TIME_MS.with(|t| {
1981            if let Some(base_time) = t.get() {
1982                let counter = SIMULATION_TIME_COUNTER.with(|c| c.get());
1983                base_time.saturating_add(counter)
1984            } else {
1985                use std::time::{SystemTime, UNIX_EPOCH};
1986                SystemTime::now()
1987                    .duration_since(UNIX_EPOCH)
1988                    .expect("system time before unix epoch")
1989                    .as_millis() as u64
1990            }
1991        })
1992    }
1993
1994    /// Returns true if simulation time is set (thread-local).
1995    pub fn is_simulation_time() -> bool {
1996        SIMULATION_TIME_MS.with(|t| t.get().is_some())
1997    }
1998
1999    /// Generates a deterministic ULID using GlobalRng and simulation time.
2000    ///
2001    /// When both GlobalRng and GlobalSimulationTime are configured:
2002    /// - Timestamp: Uses simulation time base + monotonic counter
2003    /// - Random: Uses seeded RNG from GlobalRng
2004    ///
2005    /// When not in simulation mode, uses regular `Ulid::new()`.
2006    pub fn new_ulid() -> ulid::Ulid {
2007        use ulid::Ulid;
2008
2009        if GlobalRng::is_seeded() || Self::is_simulation_time() {
2010            // Deterministic mode: construct ULID manually
2011            let timestamp_ms = Self::current_time_ms();
2012
2013            // Generate 80 bits of random data using GlobalRng
2014            let mut random_bytes = [0u8; 10];
2015            GlobalRng::fill_bytes(&mut random_bytes);
2016
2017            // Construct ULID: 48-bit timestamp (ms) + 80-bit random
2018            // ULID format: TTTTTTTTTTRRRRRRRRRRRRRRRRRRRRR (T=timestamp, R=random)
2019            let ts = (timestamp_ms as u128) << 80;
2020            let rand_high = (random_bytes[0] as u128) << 72;
2021            let rand_mid = u64::from_be_bytes([
2022                random_bytes[1],
2023                random_bytes[2],
2024                random_bytes[3],
2025                random_bytes[4],
2026                random_bytes[5],
2027                random_bytes[6],
2028                random_bytes[7],
2029                random_bytes[8],
2030            ]) as u128;
2031            let rand_low = (random_bytes[9] as u128) << 56;
2032            let ulid_value = ts | rand_high | (rand_mid << 8) | rand_low;
2033
2034            Ulid(ulid_value)
2035        } else {
2036            // Production mode: use standard ULID generation
2037            Ulid::new()
2038        }
2039    }
2040}
2041
2042// =============================================================================
2043// Simulation Transport Optimization
2044// =============================================================================
2045
2046std::thread_local! {
2047    static SIMULATION_TRANSPORT_OPT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
2048}
2049
2050/// Opt-in transport timer optimization for large-scale simulations.
2051///
2052/// When enabled, the transport layer uses relaxed timer intervals (5x slower ACK,
2053/// resend, and rate-update checks) and disables keepalive pings. This dramatically
2054/// reduces tokio scheduler overhead for 100+ node simulations where ~15K connections
2055/// would otherwise create ~900K timer firings per second of virtual time.
2056///
2057/// This is a separate flag from `GlobalSimulationTime` because some simulation tests
2058/// need realistic keepalive behavior (e.g., connection timeout tests). Only
2059/// large-scale simulations that prioritize throughput should enable this.
2060///
2061/// # Safety
2062///
2063/// Only affects code paths in `PeerConnection::recv()` and `RealTime::supports_keepalive()`.
2064/// Production code never sets this flag — it is only called from `run_simulation_direct()`
2065/// which is gated behind `#[cfg(any(test, feature = "testing"))]`.
2066pub struct SimulationTransportOpt;
2067
2068impl SimulationTransportOpt {
2069    /// Enable relaxed transport timers for the current thread.
2070    pub fn enable() {
2071        SIMULATION_TRANSPORT_OPT.with(|f| f.set(true));
2072    }
2073
2074    /// Disable relaxed transport timers (restore production behavior).
2075    pub fn disable() {
2076        SIMULATION_TRANSPORT_OPT.with(|f| f.set(false));
2077    }
2078
2079    /// Returns `true` if relaxed transport timers are enabled on this thread.
2080    pub fn is_enabled() -> bool {
2081        SIMULATION_TRANSPORT_OPT.with(|f| f.get())
2082    }
2083}
2084
2085// =============================================================================
2086// Global Test Metrics (for simulation testing)
2087// =============================================================================
2088
2089// Thread-local test metrics: allows parallel simulation tests without interference.
2090std::thread_local! {
2091    static GLOBAL_RESYNC_REQUESTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2092    static GLOBAL_DELTA_SENDS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2093    static GLOBAL_FULL_STATE_SENDS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2094    static GLOBAL_PENDING_OP_INSERTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2095    static GLOBAL_PENDING_OP_REMOVES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2096    static GLOBAL_PENDING_OP_HWM: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2097    static GLOBAL_NEIGHBOR_CACHE_UPDATES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2098    static GLOBAL_ANTI_STARVATION_TRIGGERS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2099}
2100
2101/// Global test metrics for tracking events across the simulation network.
2102///
2103/// These counters are incremented by production code and read by tests to verify
2104/// correct behavior. They should only be used in testing scenarios.
2105///
2106/// # Usage in Tests
2107///
2108/// ```ignore
2109/// use freenet::config::GlobalTestMetrics;
2110///
2111/// // Reset at test start
2112/// GlobalTestMetrics::reset();
2113///
2114/// // Run simulation...
2115///
2116/// // Check results
2117/// assert_eq!(GlobalTestMetrics::resync_requests(), 0,
2118///     "No resyncs should be needed with correct summary caching");
2119/// ```
2120pub struct GlobalTestMetrics;
2121
2122impl GlobalTestMetrics {
2123    /// Resets all test metrics to zero (thread-local). Call at the start of each test.
2124    pub fn reset() {
2125        GLOBAL_RESYNC_REQUESTS.with(|c| c.set(0));
2126        GLOBAL_DELTA_SENDS.with(|c| c.set(0));
2127        GLOBAL_FULL_STATE_SENDS.with(|c| c.set(0));
2128        GLOBAL_PENDING_OP_INSERTS.with(|c| c.set(0));
2129        GLOBAL_PENDING_OP_REMOVES.with(|c| c.set(0));
2130        GLOBAL_PENDING_OP_HWM.with(|c| c.set(0));
2131        GLOBAL_NEIGHBOR_CACHE_UPDATES.with(|c| c.set(0));
2132        GLOBAL_ANTI_STARVATION_TRIGGERS.with(|c| c.set(0));
2133    }
2134
2135    /// Records that a ResyncRequest was received.
2136    /// Called from production code when handling ResyncRequest messages.
2137    pub fn record_resync_request() {
2138        GLOBAL_RESYNC_REQUESTS.with(|c| c.set(c.get() + 1));
2139    }
2140
2141    /// Returns the total number of ResyncRequests received since last reset.
2142    pub fn resync_requests() -> u64 {
2143        GLOBAL_RESYNC_REQUESTS.with(|c| c.get())
2144    }
2145
2146    /// Records that a delta was sent in a state change broadcast.
2147    /// Called from p2p_protoc.rs when sent_delta = true.
2148    pub fn record_delta_send() {
2149        GLOBAL_DELTA_SENDS.with(|c| c.set(c.get() + 1));
2150    }
2151
2152    /// Returns the total number of delta sends since last reset.
2153    pub fn delta_sends() -> u64 {
2154        GLOBAL_DELTA_SENDS.with(|c| c.get())
2155    }
2156
2157    /// Records that full state was sent in a state change broadcast.
2158    /// Called from p2p_protoc.rs when sent_delta = false.
2159    pub fn record_full_state_send() {
2160        GLOBAL_FULL_STATE_SENDS.with(|c| c.set(c.get() + 1));
2161    }
2162
2163    /// Returns the total number of full state sends since last reset.
2164    pub fn full_state_sends() -> u64 {
2165        GLOBAL_FULL_STATE_SENDS.with(|c| c.get())
2166    }
2167
2168    pub fn record_pending_op_insert() {
2169        GLOBAL_PENDING_OP_INSERTS.with(|c| c.set(c.get() + 1));
2170    }
2171
2172    pub fn pending_op_inserts() -> u64 {
2173        GLOBAL_PENDING_OP_INSERTS.with(|c| c.get())
2174    }
2175
2176    pub fn record_pending_op_remove() {
2177        GLOBAL_PENDING_OP_REMOVES.with(|c| c.set(c.get() + 1));
2178    }
2179
2180    pub fn pending_op_removes() -> u64 {
2181        GLOBAL_PENDING_OP_REMOVES.with(|c| c.get())
2182    }
2183
2184    /// Track high-water mark for pending_op_results size.
2185    pub fn record_pending_op_size(len: u64) {
2186        GLOBAL_PENDING_OP_HWM.with(|c| c.set(c.get().max(len)));
2187    }
2188
2189    pub fn pending_op_high_water_mark() -> u64 {
2190        GLOBAL_PENDING_OP_HWM.with(|c| c.get())
2191    }
2192
2193    pub fn record_neighbor_cache_update() {
2194        GLOBAL_NEIGHBOR_CACHE_UPDATES.with(|c| c.set(c.get() + 1));
2195    }
2196
2197    pub fn neighbor_cache_updates() -> u64 {
2198        GLOBAL_NEIGHBOR_CACHE_UPDATES.with(|c| c.get())
2199    }
2200
2201    pub fn record_anti_starvation_trigger() {
2202        GLOBAL_ANTI_STARVATION_TRIGGERS.with(|c| c.set(c.get() + 1));
2203    }
2204
2205    pub fn anti_starvation_triggers() -> u64 {
2206        GLOBAL_ANTI_STARVATION_TRIGGERS.with(|c| c.get())
2207    }
2208}
2209
2210pub fn set_logger(
2211    level: Option<tracing::level_filters::LevelFilter>,
2212    endpoint: Option<String>,
2213    log_dir: Option<&Path>,
2214) {
2215    #[cfg(feature = "trace")]
2216    {
2217        static LOGGER_SET: AtomicBool = AtomicBool::new(false);
2218        if LOGGER_SET
2219            .compare_exchange(
2220                false,
2221                true,
2222                std::sync::atomic::Ordering::Release,
2223                std::sync::atomic::Ordering::SeqCst,
2224            )
2225            .is_err()
2226        {
2227            return;
2228        }
2229
2230        crate::tracing::tracer::init_tracer(level, endpoint, log_dir)
2231            .expect("failed tracing initialization")
2232    }
2233}
2234
2235async fn load_gateways_from_index(url: &str, pub_keys_dir: &Path) -> anyhow::Result<Gateways> {
2236    let response = reqwest::get(url).await?.error_for_status()?.text().await?;
2237    let mut gateways: Gateways = toml::from_str(&response)?;
2238    let mut base_url = reqwest::Url::parse(url)?;
2239    base_url.set_path("");
2240    let mut valid_gateways = Vec::new();
2241
2242    for gateway in &mut gateways.gateways {
2243        gateway.location = None; // always ignore any location from files if set, it should be derived from IP
2244        let public_key_url = base_url.join(&gateway.public_key_path.to_string_lossy())?;
2245        let public_key_response = reqwest::get(public_key_url).await?.error_for_status()?;
2246        let file_name = gateway
2247            .public_key_path
2248            .file_name()
2249            .ok_or_else(|| anyhow::anyhow!("Invalid public key path"))?;
2250        let local_path = pub_keys_dir.join(file_name);
2251        let mut public_key_file = File::create(&local_path)?;
2252        let content = public_key_response.bytes().await?;
2253        std::io::copy(&mut content.as_ref(), &mut public_key_file)?;
2254
2255        // Validate the public key (hex-encoded X25519 public key, 32 bytes = 64 hex chars)
2256        // Also accept legacy RSA PEM keys temporarily for backwards compatibility
2257        let mut key_file = File::open(&local_path).with_context(|| {
2258            format!(
2259                "failed loading gateway pubkey from {:?}",
2260                gateway.public_key_path
2261            )
2262        })?;
2263        let mut buf = String::new();
2264        key_file.read_to_string(&mut buf)?;
2265        let buf = buf.trim();
2266
2267        // Check if it's a legacy RSA PEM public key
2268        if buf.starts_with("-----BEGIN") {
2269            tracing::warn!(
2270                public_key_path = ?gateway.public_key_path,
2271                "Gateway uses legacy RSA PEM public key format. \
2272                 Gateway needs to be updated to X25519 format. Skipping."
2273            );
2274            continue;
2275        }
2276
2277        if let Ok(key_bytes) = hex::decode(buf) {
2278            if key_bytes.len() == 32 {
2279                gateway.public_key_path = local_path;
2280                valid_gateways.push(gateway.clone());
2281            } else {
2282                tracing::warn!(
2283                    public_key_path = ?gateway.public_key_path,
2284                    "Invalid public key length {} (expected 32), ignoring",
2285                    key_bytes.len()
2286                );
2287            }
2288        } else {
2289            tracing::warn!(
2290                public_key_path = ?gateway.public_key_path,
2291                "Invalid public key hex encoding in remote gateway file, ignoring"
2292            );
2293        }
2294    }
2295
2296    gateways.gateways = valid_gateways;
2297    Ok(gateways)
2298}
2299
2300#[cfg(test)]
2301mod tests {
2302    use httptest::{matchers::*, responders::*, Expectation, Server};
2303
2304    use crate::node::NodeConfig;
2305    use crate::transport::TransportKeypair;
2306
2307    use super::*;
2308
2309    #[tokio::test]
2310    async fn test_serde_config_args() {
2311        // Use tempfile for a guaranteed-writable directory (avoids CI permission issues on /tmp)
2312        let temp_dir = tempfile::tempdir().unwrap();
2313        let args = ConfigArgs {
2314            mode: Some(OperationMode::Local),
2315            config_paths: ConfigPathsArgs {
2316                config_dir: Some(temp_dir.path().to_path_buf()),
2317                data_dir: Some(temp_dir.path().to_path_buf()),
2318                log_dir: Some(temp_dir.path().to_path_buf()),
2319            },
2320            ..Default::default()
2321        };
2322        let cfg = args.build().await.unwrap();
2323        let serialized = toml::to_string(&cfg).unwrap();
2324        let _: Config = toml::from_str(&serialized).unwrap();
2325    }
2326
2327    #[tokio::test]
2328    async fn test_load_gateways_from_index() {
2329        let server = Server::run();
2330        server.expect(
2331            Expectation::matching(all_of!(request::method("GET"), request::path("/gateways")))
2332                .respond_with(status_code(200).body(
2333                    r#"
2334                    [[gateways]]
2335                    address = { hostname = "example.com" }
2336                    public_key = "/path/to/public_key.pem"
2337                    "#,
2338                )),
2339        );
2340
2341        let url = server.url_str("/gateways");
2342
2343        // Generate a valid X25519 public key in hex format
2344        let keypair = TransportKeypair::new();
2345        let key_hex = hex::encode(keypair.public().as_bytes());
2346        server.expect(
2347            Expectation::matching(request::path("/path/to/public_key.pem"))
2348                .respond_with(status_code(200).body(key_hex)),
2349        );
2350
2351        let pub_keys_dir = tempfile::tempdir().unwrap();
2352        let gateways = load_gateways_from_index(&url, pub_keys_dir.path())
2353            .await
2354            .unwrap();
2355
2356        assert_eq!(gateways.gateways.len(), 1);
2357        assert_eq!(
2358            gateways.gateways[0].address,
2359            Address::Hostname("example.com".to_string())
2360        );
2361        assert_eq!(
2362            gateways.gateways[0].public_key_path,
2363            pub_keys_dir.path().join("public_key.pem")
2364        );
2365        assert!(pub_keys_dir.path().join("public_key.pem").exists());
2366    }
2367
2368    #[test]
2369    fn test_gateways() {
2370        let gateways = Gateways {
2371            gateways: vec![
2372                GatewayConfig {
2373                    address: Address::HostAddress(
2374                        ([127, 0, 0, 1], default_network_api_port()).into(),
2375                    ),
2376                    public_key_path: PathBuf::from("path/to/key"),
2377                    location: None,
2378                },
2379                GatewayConfig {
2380                    address: Address::Hostname("technic.locut.us".to_string()),
2381                    public_key_path: PathBuf::from("path/to/key"),
2382                    location: None,
2383                },
2384            ],
2385        };
2386
2387        let serialized = toml::to_string(&gateways).unwrap();
2388        let _: Gateways = toml::from_str(&serialized).unwrap();
2389    }
2390
2391    #[tokio::test]
2392    #[ignore = "Requires gateway keys to be updated to X25519 format (issue #2531)"]
2393    async fn test_remote_freenet_gateways() {
2394        let tmp_dir = tempfile::tempdir().unwrap();
2395        let gateways = load_gateways_from_index(FREENET_GATEWAYS_INDEX, tmp_dir.path())
2396            .await
2397            .unwrap();
2398        assert!(!gateways.gateways.is_empty());
2399
2400        for gw in gateways.gateways {
2401            assert!(gw.public_key_path.exists());
2402            // Validate the public key is in hex format (32 bytes = 64 hex chars)
2403            let key_contents = std::fs::read_to_string(&gw.public_key_path).unwrap();
2404            let key_bytes =
2405                hex::decode(key_contents.trim()).expect("Gateway public key should be valid hex");
2406            assert_eq!(
2407                key_bytes.len(),
2408                32,
2409                "Gateway public key should be 32 bytes (X25519)"
2410            );
2411            let socket = NodeConfig::parse_socket_addr(&gw.address).await.unwrap();
2412            // Don't test for specific port since it's randomly assigned
2413            assert!(socket.port() > 1024); // Ensure we're using unprivileged ports
2414        }
2415    }
2416
2417    #[test]
2418    fn test_streaming_config_defaults_via_serde() {
2419        let minimal_config = r#"
2420            network-address = "127.0.0.1"
2421            network-port = 8080
2422        "#;
2423        let network_api: NetworkApiConfig = toml::from_str(minimal_config).unwrap();
2424        assert_eq!(
2425            network_api.streaming_threshold,
2426            64 * 1024,
2427            "Default streaming threshold should be 64KB"
2428        );
2429    }
2430
2431    #[test]
2432    fn test_streaming_config_serde() {
2433        let config_str = r#"
2434            network-address = "127.0.0.1"
2435            network-port = 8080
2436            streaming-threshold = 131072
2437        "#;
2438
2439        let config: NetworkApiConfig = toml::from_str(config_str).unwrap();
2440        assert_eq!(config.streaming_threshold, 128 * 1024);
2441
2442        let serialized = toml::to_string(&config).unwrap();
2443        assert!(serialized.contains("streaming-threshold = 131072"));
2444    }
2445
2446    #[test]
2447    fn test_network_args_streaming_defaults() {
2448        let args = NetworkArgs::default();
2449        assert!(
2450            args.streaming_threshold.is_none(),
2451            "NetworkArgs.streaming_threshold should be None by default"
2452        );
2453    }
2454
2455    #[test]
2456    fn test_congestion_control_config_defaults() {
2457        // Verify default congestion control is fixedrate
2458        let config_str = r#"
2459            network-address = "127.0.0.1"
2460            network-port = 8080
2461        "#;
2462        let network_api: NetworkApiConfig = toml::from_str(config_str).unwrap();
2463        assert_eq!(
2464            network_api.congestion_control, "fixedrate",
2465            "Default congestion control should be fixedrate"
2466        );
2467        assert!(
2468            network_api.bbr_startup_rate.is_none(),
2469            "Default BBR startup rate should be None"
2470        );
2471
2472        // Build the congestion config and verify the algorithm
2473        let cc_config = network_api.build_congestion_config();
2474        assert_eq!(cc_config.algorithm, CongestionControlAlgorithm::FixedRate);
2475    }
2476
2477    #[test]
2478    fn test_congestion_control_config_bbr() {
2479        // Test BBR configuration with custom startup rate
2480        let config_str = r#"
2481            network-address = "127.0.0.1"
2482            network-port = 8080
2483            congestion-control = "bbr"
2484            bbr-startup-rate = 10000000
2485        "#;
2486
2487        let config: NetworkApiConfig = toml::from_str(config_str).unwrap();
2488        assert_eq!(config.congestion_control, "bbr");
2489        assert_eq!(config.bbr_startup_rate, Some(10_000_000));
2490
2491        // Build the congestion config and verify BBR with custom startup rate
2492        let cc_config = config.build_congestion_config();
2493        assert_eq!(cc_config.algorithm, CongestionControlAlgorithm::Bbr);
2494    }
2495
2496    #[test]
2497    fn test_congestion_control_config_ledbat() {
2498        // Test LEDBAT configuration
2499        let config_str = r#"
2500            network-address = "127.0.0.1"
2501            network-port = 8080
2502            congestion-control = "ledbat"
2503        "#;
2504
2505        let config: NetworkApiConfig = toml::from_str(config_str).unwrap();
2506        assert_eq!(config.congestion_control, "ledbat");
2507
2508        let cc_config = config.build_congestion_config();
2509        assert_eq!(cc_config.algorithm, CongestionControlAlgorithm::Ledbat);
2510    }
2511
2512    #[test]
2513    fn test_congestion_control_config_serde_roundtrip() {
2514        // Test serialization/deserialization of congestion control config
2515        let config_str = r#"
2516            network-address = "127.0.0.1"
2517            network-port = 8080
2518            congestion-control = "bbr"
2519            bbr-startup-rate = 5000000
2520        "#;
2521
2522        let config: NetworkApiConfig = toml::from_str(config_str).unwrap();
2523
2524        // Round-trip test
2525        let serialized = toml::to_string(&config).unwrap();
2526        assert!(serialized.contains("congestion-control = \"bbr\""));
2527        assert!(serialized.contains("bbr-startup-rate = 5000000"));
2528
2529        // Deserialize again and verify
2530        let config2: NetworkApiConfig = toml::from_str(&serialized).unwrap();
2531        assert_eq!(config2.congestion_control, "bbr");
2532        assert_eq!(config2.bbr_startup_rate, Some(5_000_000));
2533    }
2534
2535    #[test]
2536    fn test_set_seed_pins_thread_index_to_zero() {
2537        GlobalRng::clear_seed();
2538
2539        GlobalRng::set_seed(0xDEAD_BEEF);
2540        assert_eq!(GlobalRng::thread_index(), 0);
2541
2542        // Same seed produces same RNG output
2543        let val1 = GlobalRng::random_u64();
2544        GlobalRng::set_seed(0xDEAD_BEEF);
2545        let val2 = GlobalRng::random_u64();
2546        assert_eq!(val1, val2);
2547
2548        GlobalRng::clear_seed();
2549    }
2550}