1use 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
56pub struct DryRunResult {
58 pub description: String,
60 pub result: Result<(), anyhow::Error>,
62}
63
64const 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
82pub fn resolve_max_memory(config: &mut Config) {
84 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
101pub 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 #[derive(Debug, Deserialize, Serialize, PartialEq)]
131 #[serde(deny_unknown_fields, default)]
132 pub struct Config {
133 pub network: Network,
137
138 pub offline: bool,
146
147 pub fast_sync: bool,
157
158 #[comment_out = true]
159 pub fixed_difficulty: u128,
167
168 pub target_max_memory: DefaultOrCustom<u64>,
178
179 #[child = true]
180 pub tracing: TracingConfig,
184
185 #[child = true]
186 pub tokio: TokioConfig,
190
191 #[child = true]
192 pub rayon: RayonConfig,
196
197 #[child = true]
198 pub p2p: P2PConfig,
200
201 #[child = true]
202 pub tor: TorConfig,
204
205 #[child = true]
206 pub rpc: RpcConfig,
208
209 #[child = true]
210 pub storage: StorageConfig,
212
213 #[child = true]
214 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 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 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 pub const fn network(&self) -> Network {
270 self.network
271 }
272
273 pub fn fast_sync_hashes(&self) -> &'static [[u8; 32]] {
276 crate::blockchain::get_fast_sync_hashes(self.fast_sync, self.network)
277 }
278
279 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 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 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 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 pub fn fjall_directory(&self) -> PathBuf {
381 path_with_network(&self.fs.fast_data_directory, self.network).join("fjall")
382 }
383
384 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 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 pub fn block_downloader_config(&self) -> BlockDownloaderConfig {
416 self.p2p
417 .block_downloader
418 .construct_inner(self.target_max_memory())
419 }
420
421 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 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 assert!(!path.exists());
612 assert!(Config::check_dir_permissions(&path).is_ok());
613 assert!(path.exists());
614
615 assert!(Config::check_dir_permissions(&path).is_ok());
617 }
618}