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        }
377    }
378
379    /// The directory for fjall.
380    pub fn fjall_directory(&self) -> PathBuf {
381        path_with_network(&self.fs.fast_data_directory, self.network).join("fjall")
382    }
383
384    /// Returns the size of the fjall cache.
385    ///
386    /// # Panics
387    ///
388    /// Panics if `target_max_memory` is unresolved.
389    pub fn fjall_cache_size(&self) -> u64 {
390        *self
391            .storage
392            .fjall_cache_size
393            .value(&(self.target_max_memory() / 4))
394    }
395
396    /// Returns the target maximum memory usage.
397    ///
398    /// # Panics
399    ///
400    /// Panics if `target_max_memory` is unresolved.
401    pub fn target_max_memory(&self) -> u64 {
402        match self.target_max_memory {
403            DefaultOrCustom::Default => {
404                panic!("`target_max_memory` is unresolved; call `resolve_max_memory` first")
405            }
406            DefaultOrCustom::Custom(size) => size,
407        }
408    }
409
410    /// The [`BlockDownloaderConfig`].
411    ///
412    /// # Panics
413    ///
414    /// Panics if `target_max_memory` is unresolved.
415    pub fn block_downloader_config(&self) -> BlockDownloaderConfig {
416        self.p2p
417            .block_downloader
418            .construct_inner(self.target_max_memory())
419    }
420
421    /// Checks if a port can be bound to.
422    /// Returns `Ok(())` if the port is available, otherwise returns an error.
423    fn check_port(ip: IpAddr, port: u16) -> Result<(), anyhow::Error> {
424        match TcpListener::bind((ip, port)) {
425            Ok(_) => Ok(()),
426            Err(e) => {
427                bail!("Failed to bind {ip}:{port} - {e}")
428            }
429        }
430    }
431
432    /// Create directory at path if it doesn't exists.
433    /// Checks if directory has proper read/write permissions.
434    fn check_dir_permissions(path: &Path) -> Result<(), anyhow::Error> {
435        if !path.exists() {
436            if let Err(e) = std::fs::create_dir_all(path) {
437                bail!("Cannot create directory {}: {e}", path.display());
438            }
439        }
440
441        let metadata = match std::fs::metadata(path) {
442            Ok(m) => m,
443            Err(e) => bail!("Cannot access {}: {e}", path.display()),
444        };
445
446        if !metadata.is_dir() {
447            bail!("Path {} is not a directory", path.display());
448        }
449
450        if let Err(e) = std::fs::read_dir(path) {
451            bail!("No read permission for {}", path.display())
452        }
453
454        let test_file = path.join(".cuprate_write_test");
455        if let Err(e) = std::fs::write(&test_file, b"Cuprate") {
456            bail!("No write permission for {}", path.display());
457        }
458
459        if let Err(e) = std::fs::remove_file(&test_file) {
460            bail!("Cannot remove temporary file from {}", path.display());
461        }
462
463        Ok(())
464    }
465
466    pub fn dry_run_check(&self) -> Vec<DryRunResult> {
467        let mut results = Vec::new();
468
469        if !self.offline && self.p2p.clear_net.enable_inbound {
470            let port = p2p_port(self.p2p.clear_net.p2p_port, self.network);
471            let ip = self.p2p.clear_net.listen_on;
472
473            results.push(DryRunResult {
474                description: format!("P2P clearnet {ip}:{port} available."),
475                result: Self::check_port(IpAddr::V4(ip), port),
476            });
477        }
478
479        if !self.offline && self.p2p.clear_net.enable_inbound_v6 {
480            let port = p2p_port(self.p2p.clear_net.p2p_port, self.network);
481            let ip = self.p2p.clear_net.listen_on_v6;
482
483            results.push(DryRunResult {
484                description: format!("P2P clearnet {ip}:{port} available."),
485                result: Self::check_port(IpAddr::V6(ip), port),
486            });
487        }
488
489        if self.rpc.restricted.enable {
490            let port = restricted_rpc_port(self.rpc.restricted.port, self.network);
491            let ip = self.rpc.restricted.address;
492
493            results.push(DryRunResult {
494                description: format!("RPC restricted {ip}:{port} available."),
495                result: Self::check_port(ip, port),
496            });
497        }
498
499        if self.rpc.unrestricted.enable {
500            let port = unrestricted_rpc_port(self.rpc.unrestricted.port, self.network);
501            let ip = self.rpc.unrestricted.address;
502
503            results.push(DryRunResult {
504                description: format!("RPC unrestricted {ip}:{port} available."),
505                result: Self::check_port(ip, port),
506            });
507        }
508
509        if !self.offline && self.tor.mode == TorMode::Daemon {
510            let port = self.tor.daemon.listening_addr.port();
511            let ip = self.tor.daemon.listening_addr.ip();
512
513            results.push(DryRunResult {
514                description: format!("Tor daemon {ip}:{port} available."),
515                result: Self::check_port(ip, port),
516            });
517        }
518
519        results.push(DryRunResult {
520            description: format!(
521                "File permissions are valid at {}",
522                self.fs.fast_data_directory.display()
523            ),
524            result: Self::check_dir_permissions(&self.fs.fast_data_directory),
525        });
526
527        results.push(DryRunResult {
528            description: format!(
529                "File permissions are valid at {}",
530                self.fs.slow_data_directory.display()
531            ),
532            result: Self::check_dir_permissions(&self.fs.slow_data_directory),
533        });
534
535        results.push(DryRunResult {
536            description: format!(
537                "File permissions are valid at {}",
538                self.fs.cache_directory.display()
539            ),
540            result: Self::check_dir_permissions(&self.fs.cache_directory),
541        });
542
543        #[cfg(feature = "arti")]
544        if matches!(self.tor.mode, TorMode::Arti | TorMode::Auto) {
545            results.push(DryRunResult {
546                description: format!(
547                    "File permissions are valid at {}",
548                    self.tor.arti.directory_path.display()
549                ),
550                result: Self::check_dir_permissions(&self.tor.arti.directory_path),
551            });
552        }
553
554        results
555    }
556}
557
558impl fmt::Display for Config {
559    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560        writeln!(
561            f,
562            "========== CONFIGURATION ==========\n{self:#?}\n==================================="
563        )
564    }
565}
566
567#[cfg(test)]
568mod test {
569    use pretty_assertions::assert_eq;
570    use std::fs;
571    use tempfile::tempdir;
572    use toml::{from_str, to_string};
573
574    use super::*;
575
576    #[test]
577    fn documented_config() {
578        let str = Config::documented_config();
579        let conf: Config = from_str(&str).unwrap();
580
581        assert_eq!(conf, Config::default());
582    }
583
584    #[test]
585    fn test_check_port() {
586        let port = 18080;
587        let ip = IpAddr::from_str("127.0.0.1").unwrap();
588        assert!(Config::check_port(ip, port).is_ok());
589
590        let _listener = TcpListener::bind((ip, port)).expect("fail to bind to the port for test");
591        assert!(Config::check_port(ip, port).is_err());
592    }
593
594    #[test]
595    fn test_read_from_path() {
596        let tmp_dir = tempdir().unwrap();
597        let config_path = tmp_dir.path().join("config.toml");
598        let config_str = to_string(&Config::default()).unwrap();
599        fs::write(&config_path, config_str).unwrap();
600
601        let config = Config::read_from_path(config_path).unwrap();
602        assert_eq!(config, Config::default());
603    }
604
605    #[test]
606    fn test_check_file_permissions() {
607        let tmp_dir = tempdir().unwrap();
608        let path = tmp_dir.path().join("new_dir");
609
610        // Test on non existing directory
611        assert!(!path.exists());
612        assert!(Config::check_dir_permissions(&path).is_ok());
613        assert!(path.exists());
614
615        // Test on an existing directory
616        assert!(Config::check_dir_permissions(&path).is_ok());
617    }
618}