Skip to main content

cuprated/
config.rs

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