1use 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
50pub struct DryRunResult {
52 pub description: String,
54 pub result: Result<(), anyhow::Error>,
56}
57
58const 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
76pub fn resolve_max_memory(config: &mut Config) -> Result<(), anyhow::Error> {
82 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
99pub 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 #[derive(Debug, Deserialize, Serialize, PartialEq)]
129 #[serde(deny_unknown_fields, default)]
130 pub struct Config {
131 pub network: Network,
135
136 pub offline: bool,
144
145 pub fast_sync: bool,
155
156 #[comment_out = true]
157 pub fixed_difficulty: u128,
165
166 pub target_max_memory: DefaultOrCustom<u64>,
176
177 #[child = true]
178 pub tracing: TracingConfig,
182
183 #[child = true]
184 pub tokio: TokioConfig,
188
189 #[child = true]
190 pub rayon: RayonConfig,
194
195 #[child = true]
196 pub p2p: P2PConfig,
198
199 #[child = true]
200 pub tor: TorConfig,
202
203 #[child = true]
204 pub rpc: RpcConfig,
206
207 #[child = true]
208 pub storage: StorageConfig,
210
211 #[child = true]
212 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 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 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 pub const fn network(&self) -> Network {
273 self.network
274 }
275
276 pub fn fast_sync_hashes(&self) -> &'static [[u8; 32]] {
279 crate::blockchain::get_fast_sync_hashes(self.fast_sync, self.network)
280 }
281
282 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 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 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 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 pub fn fjall_directory(&self) -> PathBuf {
384 path_with_network(&self.fs.fast_data_directory, self.network).join("fjall")
385 }
386
387 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 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 pub fn block_downloader_config(&self) -> BlockDownloaderConfig {
419 self.p2p
420 .block_downloader
421 .construct_inner(self.target_max_memory())
422 }
423
424 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 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 assert!(!path.exists());
615 assert!(Config::check_dir_permissions(&path).is_ok());
616 assert!(path.exists());
617
618 assert!(Config::check_dir_permissions(&path).is_ok());
620 }
621}