Skip to main content

cuprated/
config.rs

1//! cuprated config
2use std::{
3    fmt,
4    fs::{read_to_string, File},
5    io,
6    net::{IpAddr, TcpListener},
7    path::{Path, PathBuf},
8    str::FromStr,
9    time::Duration,
10};
11
12use anyhow::{bail, Context};
13use cuprate_blockchain::config::CacheSizes;
14use serde::{Deserialize, Serialize};
15
16use cuprate_consensus::ContextConfig;
17use cuprate_helper::{
18    fs::{path_with_network, CUPRATE_CONFIG_DIR, DEFAULT_CONFIG_FILE_NAME},
19    network::Network,
20};
21use cuprate_p2p::block_downloader::BlockDownloaderConfig;
22use cuprate_p2p_core::{ClearNet, Tor};
23use cuprate_wire::OnionAddr;
24
25use crate::{
26    logging::eprintln_red,
27    tor::{TorContext, TorMode},
28};
29
30#[cfg(feature = "arti")]
31use {arti_client::KeystoreSelector, safelog::DisplayRedacted};
32
33mod default;
34mod fs;
35mod p2p;
36mod rayon;
37mod rpc;
38mod storage;
39mod tokio;
40mod tor;
41mod tracing_config;
42
43#[macro_use]
44mod macros;
45
46use default::DefaultOrCustom;
47use fs::FileSystemConfig;
48pub use p2p::{p2p_port, P2PConfig};
49use rayon::RayonConfig;
50pub use rpc::{restricted_rpc_port, unrestricted_rpc_port, RpcConfig};
51pub use storage::{StorageConfig, TxpoolConfig};
52use tokio::TokioConfig;
53use tor::TorConfig;
54use tracing_config::TracingConfig;
55
56/// Result of a single check from [`Config::dry_run_check`].
57pub struct DryRunResult {
58    /// Description of the check.
59    pub description: String,
60    /// The result of the check.
61    pub result: Result<(), anyhow::Error>,
62}
63
64/// Header to put at the start of the generated config file.
65const HEADER: &str = r"##     ____                      _
66##    / ___|   _ _ __  _ __ __ _| |_ ___
67##   | |  | | | | '_ \| '__/ _` | __/ _ \
68##   | |__| |_| | |_) | | | (_| | ||  __/
69##    \____\__,_| .__/|_|  \__,_|\__\___|
70##              |_|
71##
72## All these config values can be set to
73## their default by commenting them out with '#'.
74##
75## Some values are already commented out,
76## to set the value remove the '#' at the start of the line.
77##
78## For more documentation, see: <https://user.cuprate.org>.
79
80";
81
82/// Resolves `target_max_memory` from system RAM if unset.
83pub fn resolve_max_memory(config: &mut Config) {
84    // TODO: don't use `DefaultOrCustom` for target_max_memory.
85    if matches!(config.target_max_memory, DefaultOrCustom::Default) {
86        tracing::info!("Attempting to read total memory from system");
87
88        let mut info = sysinfo::System::new();
89        info.refresh_memory();
90        let memory = info.total_memory();
91
92        if memory == 0 {
93            eprintln_red("Unable to read total memory, please manually set the `target_max_memory` value in the config file.");
94            std::process::exit(1);
95        }
96
97        config.target_max_memory = DefaultOrCustom::Custom(memory);
98    }
99}
100
101/// Finds and reads a config file from the default locations.
102///
103/// Tries the current directory first, then the config directory.
104/// Returns `None` if no config file is found in either location.
105///
106/// # Errors
107///
108/// Returns an error if a config file is found but cannot be parsed.
109pub fn find_config() -> Result<Option<Config>, anyhow::Error> {
110    let paths = [
111        std::env::current_dir()
112            .ok()
113            .map(|p| p.join(DEFAULT_CONFIG_FILE_NAME)),
114        Some(CUPRATE_CONFIG_DIR.join(DEFAULT_CONFIG_FILE_NAME)),
115    ];
116
117    for path in paths.into_iter().flatten() {
118        if !path.exists() {
119            continue;
120        }
121
122        return Config::read_from_path(&path).map(Some);
123    }
124
125    Ok(None)
126}
127
128config_struct! {
129    /// The config for all of Cuprate.
130    #[derive(Debug, Deserialize, Serialize, PartialEq)]
131    #[serde(deny_unknown_fields, default)]
132    pub struct Config {
133        /// The network cuprated should run on.
134        ///
135        /// Valid values | "Mainnet", "Testnet", "Stagenet", "FakeChain"
136        pub network: Network,
137
138        /// Run the node offline.
139        ///
140        /// No connections will be made to or accepted from peers,
141        /// on any network zone.
142        ///
143        /// Type         | boolean
144        /// Valid values | true, false
145        pub offline: bool,
146
147        /// Enable/disable fast sync.
148        ///
149        /// Fast sync skips verification of old blocks by
150        /// comparing block hashes to a built-in hash file,
151        /// disabling this will significantly increase sync time.
152        /// New blocks are still fully validated.
153        ///
154        /// Type         | boolean
155        /// Valid values | true, false
156        pub fast_sync: bool,
157
158        #[comment_out = true]
159        /// Fixes the PoW difficulty to this value.
160        ///
161        /// Only intended for regtest (`network = "FakeChain"`). A value of
162        /// `0` disables this override.
163        ///
164        /// Type         | Number
165        /// Valid values | >= 0
166        pub fixed_difficulty: u128,
167
168        /// The target maximum amount of memory to use in bytes.
169        ///
170        /// This is not a hard limit, but Cuprate will attempt to stay under this value.
171        /// You probably do not need to change this unless Cuprate can't read the amount of RAM your
172        /// system has.
173        ///
174        /// Type         | Number
175        /// Valid values | > 0
176        /// Examples     | 500_000_000, 1_000_000_000,
177        pub target_max_memory: DefaultOrCustom<u64>,
178
179        #[child = true]
180        /// Configuration for cuprated's logging system, tracing.
181        ///
182        /// Tracing is used for logging to stdout and files.
183        pub tracing: TracingConfig,
184
185        #[child = true]
186        /// Configuration for cuprated's asynchronous runtime system, tokio.
187        ///
188        /// Tokio is used for network operations and the major services inside `cuprated`.
189        pub tokio: TokioConfig,
190
191        #[child = true]
192        /// Configuration for cuprated's thread-pool system, rayon.
193        ///
194        /// Rayon is used for CPU intensive tasks.
195        pub rayon: RayonConfig,
196
197        #[child = true]
198        /// Configuration for cuprated's P2P system.
199        pub p2p: P2PConfig,
200
201        #[child = true]
202        /// Configuration for cuprated's Tor component
203        pub tor: TorConfig,
204
205        #[child = true]
206        /// Configuration for cuprated's RPC system.
207        pub rpc: RpcConfig,
208
209        #[child = true]
210        /// Configuration for persistent data storage.
211        pub storage: StorageConfig,
212
213        #[child = true]
214        /// Configuration for the file-system.
215        pub fs: FileSystemConfig,
216    }
217}
218
219impl Default for Config {
220    fn default() -> Self {
221        Self {
222            network: Default::default(),
223            offline: false,
224            fixed_difficulty: 0,
225            fast_sync: true,
226            target_max_memory: DefaultOrCustom::Default,
227            tracing: Default::default(),
228            tokio: Default::default(),
229            tor: Default::default(),
230            rayon: Default::default(),
231            p2p: Default::default(),
232            rpc: Default::default(),
233            storage: Default::default(),
234            fs: Default::default(),
235        }
236    }
237}
238
239impl Config {
240    /// Returns a default [`Config`], with doc comments.
241    pub fn documented_config() -> String {
242        let str = toml::ser::to_string_pretty(&Self::default()).unwrap();
243        let mut doc = toml_edit::DocumentMut::from_str(&str).unwrap();
244        Self::write_docs(doc.as_table_mut());
245        format!("{HEADER}{doc}")
246    }
247
248    /// Attempts to read a config file in [`toml`] format from the given [`Path`].
249    ///
250    /// # Errors
251    ///
252    /// Will return an [`Err`] if the file cannot be read or if the file is not a valid [`toml`] config.
253    pub fn read_from_path(file: impl AsRef<Path>) -> Result<Self, anyhow::Error> {
254        let file_text = read_to_string(file.as_ref())?;
255
256        let config: Self = toml::from_str(&file_text).with_context(|| {
257            format!(
258                "Failed to parse config file at: {}",
259                file.as_ref().to_string_lossy()
260            )
261        })?;
262
263        println!("Using config at: {}", file.as_ref().to_string_lossy());
264
265        Ok(config)
266    }
267
268    /// Returns the current [`Network`] we are running on.
269    pub const fn network(&self) -> Network {
270        self.network
271    }
272
273    /// Returns the fast-sync validation hashes for this config's network,
274    /// or `&[]` if fast sync is disabled.
275    pub fn fast_sync_hashes(&self) -> &'static [[u8; 32]] {
276        crate::blockchain::get_fast_sync_hashes(self.fast_sync, self.network)
277    }
278
279    /// The [`ClearNet`], [`cuprate_p2p::P2PConfig`].
280    pub fn clearnet_p2p_config(&self) -> cuprate_p2p::P2PConfig<ClearNet> {
281        cuprate_p2p::P2PConfig {
282            network: self.network,
283            seeds: {
284                let mut seeds = p2p::clear_net_seed_nodes(self.network);
285                seeds.extend_from_slice(&self.p2p.clear_net.seed_nodes);
286                seeds
287            },
288            offline: self.offline,
289            outbound_connections: self.p2p.clear_net.outbound_connections,
290            extra_outbound_connections: self.p2p.clear_net.extra_outbound_connections,
291            max_inbound_connections: self.p2p.clear_net.max_inbound_connections,
292            gray_peers_percent: self.p2p.clear_net.gray_peers_percent,
293            p2p_port: p2p_port(self.p2p.clear_net.p2p_port, self.network),
294            rpc_port: self.rpc.restricted.port_for_p2p(self.network),
295            address_book_config: self.p2p.clear_net.address_book_config.address_book_config(
296                &self.fs.cache_directory,
297                self.network,
298                None,
299            ),
300        }
301    }
302
303    /// The [`Tor`], [`cuprate_p2p::P2PConfig`].
304    pub fn tor_p2p_config(&self, ctx: &TorContext) -> cuprate_p2p::P2PConfig<Tor> {
305        let inbound_enabled = self.p2p.tor_net.inbound_onion;
306
307        let tor_p2p_port = p2p_port(self.p2p.tor_net.p2p_port, self.network);
308
309        let our_onion_address = match ctx.mode {
310            TorMode::Daemon => inbound_enabled.then(||
311                OnionAddr::new(
312                    &self.tor.daemon.anonymous_inbound,
313                    tor_p2p_port
314                ).expect("Unable to parse supplied `anonymous_inbound` onion address. Please make sure the address is correct.")),
315            #[cfg(feature = "arti")]
316            TorMode::Arti => inbound_enabled.then(|| {
317                let addr = ctx.arti_onion_service
318                    .as_ref()
319                    .unwrap()
320                    .generate_identity_key(KeystoreSelector::Primary)
321                    .unwrap()
322                    .display_unredacted()
323                    .to_string();
324
325                OnionAddr::new(&addr, tor_p2p_port).unwrap()
326            }),
327            TorMode::Auto => unreachable!("Auto mode should be resolved before this point"),
328        };
329
330        cuprate_p2p::P2PConfig {
331            network: self.network,
332            seeds: {
333                let mut seeds = p2p::tor_net_seed_nodes(self.network);
334                seeds.extend_from_slice(&self.p2p.tor_net.seed_nodes);
335                seeds
336            },
337            offline: self.offline,
338            outbound_connections: self.p2p.tor_net.outbound_connections,
339            extra_outbound_connections: self.p2p.tor_net.extra_outbound_connections,
340            max_inbound_connections: self.p2p.tor_net.max_inbound_connections,
341            gray_peers_percent: self.p2p.tor_net.gray_peers_percent,
342            p2p_port: tor_p2p_port,
343            rpc_port: 0,
344            address_book_config: self.p2p.tor_net.address_book_config.address_book_config(
345                &self.fs.cache_directory,
346                self.network,
347                our_onion_address,
348            ),
349        }
350    }
351
352    /// The [`ContextConfig`].
353    pub const fn context_config(&self) -> ContextConfig {
354        let mut cfg = match self.network {
355            Network::Mainnet => ContextConfig::main_net(),
356            Network::Stagenet => ContextConfig::stage_net(),
357            Network::Testnet => ContextConfig::test_net(),
358            Network::FakeChain => ContextConfig::fake_chain(),
359        };
360
361        if self.fixed_difficulty != 0 {
362            cfg.difficulty_cfg.fixed_difficulty = Some(self.fixed_difficulty);
363        }
364
365        cfg
366    }
367
368    /// The [`cuprate_blockchain`] config.
369    pub fn blockchain_config(&self) -> cuprate_blockchain::config::Config {
370        let blockchain = &self.storage.blockchain;
371
372        cuprate_blockchain::config::Config {
373            blob_dir: path_with_network(&self.fs.slow_data_directory, self.network),
374            index_dir: path_with_network(&self.fs.fast_data_directory, self.network),
375            cache_sizes: self.storage.blockchain.tapes_cache_sizes.clone(),
376            persistence: self.storage.blockchain.persistence,
377        }
378    }
379
380    /// The directory for fjall.
381    pub fn fjall_directory(&self) -> PathBuf {
382        path_with_network(&self.fs.fast_data_directory, self.network).join("fjall")
383    }
384
385    /// Returns the size of the fjall cache.
386    ///
387    /// # Panics
388    ///
389    /// Panics if `target_max_memory` is unresolved.
390    pub fn fjall_cache_size(&self) -> u64 {
391        *self
392            .storage
393            .fjall_cache_size
394            .value(&(self.target_max_memory() / 4))
395    }
396
397    /// Returns the target maximum memory usage.
398    ///
399    /// # Panics
400    ///
401    /// Panics if `target_max_memory` is unresolved.
402    pub fn target_max_memory(&self) -> u64 {
403        match self.target_max_memory {
404            DefaultOrCustom::Default => {
405                panic!("`target_max_memory` is unresolved; call `resolve_max_memory` first")
406            }
407            DefaultOrCustom::Custom(size) => size,
408        }
409    }
410
411    /// The [`BlockDownloaderConfig`].
412    ///
413    /// # Panics
414    ///
415    /// Panics if `target_max_memory` is unresolved.
416    pub fn block_downloader_config(&self) -> BlockDownloaderConfig {
417        self.p2p
418            .block_downloader
419            .construct_inner(self.target_max_memory())
420    }
421
422    /// Checks if a port can be bound to.
423    /// Returns `Ok(())` if the port is available, otherwise returns an error.
424    fn check_port(ip: IpAddr, port: u16) -> Result<(), anyhow::Error> {
425        match TcpListener::bind((ip, port)) {
426            Ok(_) => Ok(()),
427            Err(e) => {
428                bail!("Failed to bind {ip}:{port} - {e}")
429            }
430        }
431    }
432
433    /// Create directory at path if it doesn't exists.
434    /// Checks if directory has proper read/write permissions.
435    fn check_dir_permissions(path: &Path) -> Result<(), anyhow::Error> {
436        if !path.exists() {
437            if let Err(e) = std::fs::create_dir_all(path) {
438                bail!("Cannot create directory {}: {e}", path.display());
439            }
440        }
441
442        let metadata = match std::fs::metadata(path) {
443            Ok(m) => m,
444            Err(e) => bail!("Cannot access {}: {e}", path.display()),
445        };
446
447        if !metadata.is_dir() {
448            bail!("Path {} is not a directory", path.display());
449        }
450
451        if let Err(e) = std::fs::read_dir(path) {
452            bail!("No read permission for {}", path.display())
453        }
454
455        let test_file = path.join(".cuprate_write_test");
456        if let Err(e) = std::fs::write(&test_file, b"Cuprate") {
457            bail!("No write permission for {}", path.display());
458        }
459
460        if let Err(e) = std::fs::remove_file(&test_file) {
461            bail!("Cannot remove temporary file from {}", path.display());
462        }
463
464        Ok(())
465    }
466
467    pub fn dry_run_check(&self) -> Vec<DryRunResult> {
468        let mut results = Vec::new();
469
470        if !self.offline && self.p2p.clear_net.enable_inbound {
471            let port = p2p_port(self.p2p.clear_net.p2p_port, self.network);
472            let ip = self.p2p.clear_net.listen_on;
473
474            results.push(DryRunResult {
475                description: format!("P2P clearnet {ip}:{port} available."),
476                result: Self::check_port(IpAddr::V4(ip), port),
477            });
478        }
479
480        if !self.offline && self.p2p.clear_net.enable_inbound_v6 {
481            let port = p2p_port(self.p2p.clear_net.p2p_port, self.network);
482            let ip = self.p2p.clear_net.listen_on_v6;
483
484            results.push(DryRunResult {
485                description: format!("P2P clearnet {ip}:{port} available."),
486                result: Self::check_port(IpAddr::V6(ip), port),
487            });
488        }
489
490        if self.rpc.restricted.enable {
491            let port = restricted_rpc_port(self.rpc.restricted.port, self.network);
492            let ip = self.rpc.restricted.address;
493
494            results.push(DryRunResult {
495                description: format!("RPC restricted {ip}:{port} available."),
496                result: Self::check_port(ip, port),
497            });
498        }
499
500        if self.rpc.unrestricted.enable {
501            let port = unrestricted_rpc_port(self.rpc.unrestricted.port, self.network);
502            let ip = self.rpc.unrestricted.address;
503
504            results.push(DryRunResult {
505                description: format!("RPC unrestricted {ip}:{port} available."),
506                result: Self::check_port(ip, port),
507            });
508        }
509
510        if !self.offline && self.tor.mode == TorMode::Daemon {
511            let port = self.tor.daemon.listening_addr.port();
512            let ip = self.tor.daemon.listening_addr.ip();
513
514            results.push(DryRunResult {
515                description: format!("Tor daemon {ip}:{port} available."),
516                result: Self::check_port(ip, port),
517            });
518        }
519
520        results.push(DryRunResult {
521            description: format!(
522                "File permissions are valid at {}",
523                self.fs.fast_data_directory.display()
524            ),
525            result: Self::check_dir_permissions(&self.fs.fast_data_directory),
526        });
527
528        results.push(DryRunResult {
529            description: format!(
530                "File permissions are valid at {}",
531                self.fs.slow_data_directory.display()
532            ),
533            result: Self::check_dir_permissions(&self.fs.slow_data_directory),
534        });
535
536        results.push(DryRunResult {
537            description: format!(
538                "File permissions are valid at {}",
539                self.fs.cache_directory.display()
540            ),
541            result: Self::check_dir_permissions(&self.fs.cache_directory),
542        });
543
544        #[cfg(feature = "arti")]
545        if matches!(self.tor.mode, TorMode::Arti | TorMode::Auto) {
546            results.push(DryRunResult {
547                description: format!(
548                    "File permissions are valid at {}",
549                    self.tor.arti.directory_path.display()
550                ),
551                result: Self::check_dir_permissions(&self.tor.arti.directory_path),
552            });
553        }
554
555        results
556    }
557}
558
559impl fmt::Display for Config {
560    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561        writeln!(
562            f,
563            "========== CONFIGURATION ==========\n{self:#?}\n==================================="
564        )
565    }
566}
567
568#[cfg(test)]
569mod test {
570    use pretty_assertions::assert_eq;
571    use std::fs;
572    use tempfile::tempdir;
573    use toml::{from_str, to_string};
574
575    use super::*;
576
577    #[test]
578    fn documented_config() {
579        let str = Config::documented_config();
580        let conf: Config = from_str(&str).unwrap();
581
582        assert_eq!(conf, Config::default());
583    }
584
585    #[test]
586    fn test_check_port() {
587        let port = 18080;
588        let ip = IpAddr::from_str("127.0.0.1").unwrap();
589        assert!(Config::check_port(ip, port).is_ok());
590
591        let _listener = TcpListener::bind((ip, port)).expect("fail to bind to the port for test");
592        assert!(Config::check_port(ip, port).is_err());
593    }
594
595    #[test]
596    fn test_read_from_path() {
597        let tmp_dir = tempdir().unwrap();
598        let config_path = tmp_dir.path().join("config.toml");
599        let config_str = to_string(&Config::default()).unwrap();
600        fs::write(&config_path, config_str).unwrap();
601
602        let config = Config::read_from_path(config_path).unwrap();
603        assert_eq!(config, Config::default());
604    }
605
606    #[test]
607    fn test_check_file_permissions() {
608        let tmp_dir = tempdir().unwrap();
609        let path = tmp_dir.path().join("new_dir");
610
611        // Test on non existing directory
612        assert!(!path.exists());
613        assert!(Config::check_dir_permissions(&path).is_ok());
614        assert!(path.exists());
615
616        // Test on an existing directory
617        assert!(Config::check_dir_permissions(&path).is_ok());
618    }
619}