// Smoldot // Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see . #![deny(rustdoc::broken_intra_doc_links)] // TODO: #![deny(unused_crate_dependencies)] doesn't work because some deps are used only by the library, figure if this can be fixed? use std::{ fs, io, sync::Arc, thread, time::{Duration, SystemTime, UNIX_EPOCH}, }; mod cli; fn main() { smol::block_on(async_main()) } async fn async_main() { match ::parse().command { cli::CliOptionsCommand::Run(r) => run(*r).await, cli::CliOptionsCommand::Blake264BitsHash(opt) => { let hash = blake2_rfc::blake2b::blake2b(8, &[], opt.payload.as_bytes()); println!("0x{}", hex::encode(hash)); } cli::CliOptionsCommand::Blake2256BitsHash(opt) => { let content = fs::read(opt.file).expect("Failed to read file content"); let hash = blake2_rfc::blake2b::blake2b(32, &[], &content); println!("0x{}", hex::encode(hash)); } } } async fn run(cli_options: cli::CliOptionsRun) { // Determine the actual CLI output by replacing `Auto` with the actual value. let cli_output = if let cli::Output::Auto = cli_options.output { if io::IsTerminal::is_terminal(&io::stderr()) && cli_options.log_level.is_none() { cli::Output::Informant } else { cli::Output::Logs } } else { cli_options.output }; debug_assert!(!matches!(cli_output, cli::Output::Auto)); // Setup the logging system of the binary. let log_callback: Arc = match cli_output { cli::Output::None => Arc::new(|_level, _message| {}), cli::Output::Informant | cli::Output::Logs => { let color_choice = cli_options.color.clone(); let log_level = cli_options.log_level.clone().unwrap_or( if matches!(cli_output, cli::Output::Informant) { cli::LogLevel::Info } else { cli::LogLevel::Debug }, ); Arc::new(move |level, message| { match (&level, &log_level) { (_, cli::LogLevel::Off) => return, ( smoldot_full_node::LogLevel::Warn | smoldot_full_node::LogLevel::Info | smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace, cli::LogLevel::Error, ) => return, ( smoldot_full_node::LogLevel::Info | smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace, cli::LogLevel::Warn, ) => return, ( smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace, cli::LogLevel::Info, ) => return, (smoldot_full_node::LogLevel::Trace, cli::LogLevel::Debug) => return, _ => {} } let when = humantime::format_rfc3339_millis(SystemTime::now()); let level_str = match (level, &color_choice) { (smoldot_full_node::LogLevel::Trace, cli::ColorChoice::Never) => "trace", (smoldot_full_node::LogLevel::Trace, cli::ColorChoice::Always) => { "\x1b[36mtrace\x1b[0m" } (smoldot_full_node::LogLevel::Debug, cli::ColorChoice::Never) => "debug", (smoldot_full_node::LogLevel::Debug, cli::ColorChoice::Always) => { "\x1b[34mdebug\x1b[0m" } (smoldot_full_node::LogLevel::Info, cli::ColorChoice::Never) => "info", (smoldot_full_node::LogLevel::Info, cli::ColorChoice::Always) => { "\x1b[32minfo\x1b[0m" } (smoldot_full_node::LogLevel::Warn, cli::ColorChoice::Never) => "warn", (smoldot_full_node::LogLevel::Warn, cli::ColorChoice::Always) => { "\x1b[33;1mwarn\x1b[0m" } (smoldot_full_node::LogLevel::Error, cli::ColorChoice::Never) => "error", (smoldot_full_node::LogLevel::Error, cli::ColorChoice::Always) => { "\x1b[31;1merror\x1b[0m" } }; eprintln!("[{}] [{}] {}", when, level_str, message); }) as Arc } cli::Output::LogsJson => { let log_level = cli_options .log_level .clone() .unwrap_or(cli::LogLevel::Debug); Arc::new(move |level, message| { match (&level, &log_level) { (_, cli::LogLevel::Off) => return, ( smoldot_full_node::LogLevel::Warn | smoldot_full_node::LogLevel::Info | smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace, cli::LogLevel::Error, ) => return, ( smoldot_full_node::LogLevel::Info | smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace, cli::LogLevel::Warn, ) => return, ( smoldot_full_node::LogLevel::Debug | smoldot_full_node::LogLevel::Trace, cli::LogLevel::Info, ) => return, (smoldot_full_node::LogLevel::Trace, cli::LogLevel::Debug) => return, _ => {} } #[derive(serde::Serialize)] struct Record { timestamp: u128, level: &'static str, message: String, } let mut lock = std::io::stderr().lock(); if serde_json::to_writer( &mut lock, &Record { timestamp: SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_millis()) .unwrap_or(0), level: match level { smoldot_full_node::LogLevel::Trace => "trace", smoldot_full_node::LogLevel::Debug => "debug", smoldot_full_node::LogLevel::Info => "info", smoldot_full_node::LogLevel::Warn => "warn", smoldot_full_node::LogLevel::Error => "error", }, message, }, ) .is_ok() { let _ = io::Write::write_all(&mut lock, b"\n"); } }) } cli::Output::Auto => unreachable!(), // Handled above. }; let chain_spec = fs::read(&cli_options.path_to_chain_spec).expect("Failed to read chain specification"); let parsed_chain_spec = { smoldot::chain_spec::ChainSpec::from_json_bytes(&chain_spec) .expect("Failed to decode chain specification") }; // Directory where we will store everything on the disk, such as the database, secret keys, // etc. let base_storage_directory = if cli_options.tmp { None } else if let Some(base) = directories::ProjectDirs::from("io", "smoldot", "smoldot") { Some(base.data_dir().to_owned()) } else { log_callback.log( smoldot_full_node::LogLevel::Warn, "Failed to fetch $HOME directory. Falling back to storing everything in memory, \ meaning that everything will be lost when the node stops. If this is intended, \ please make this explicit by passing the `--tmp` flag instead." .to_string(), ); None }; // Create the directory if necessary. if let Some(base_storage_directory) = base_storage_directory.as_ref() { fs::create_dir_all(base_storage_directory.join(parsed_chain_spec.id())).unwrap(); } // Directory supposed to contain the database. let sqlite_database_path = base_storage_directory .as_ref() .map(|d| d.join(parsed_chain_spec.id()).join("database")); // Directory supposed to contain the keystore. let keystore_path = base_storage_directory .as_ref() .map(|path| path.join(parsed_chain_spec.id()).join("keys")); // Build the relay chain information if relevant. let (relay_chain, relay_chain_name) = if let Some((relay_chain_name, _parachain_id)) = parsed_chain_spec.relay_chain() { let spec_json = { let relay_chain_path = cli_options .path_to_chain_spec .parent() .unwrap() .join(format!("{relay_chain_name}.json")); fs::read(&relay_chain_path).expect("Failed to read relay chain specification") }; let parsed_relay_spec = smoldot::chain_spec::ChainSpec::from_json_bytes(&spec_json) .expect("Failed to decode relay chain chain specs"); // Make sure we're not accidentally opening the same chain twice, otherwise weird // interactions will happen. assert_ne!(parsed_relay_spec.id(), parsed_chain_spec.id()); // Create the directory if necessary. if let Some(base_storage_directory) = base_storage_directory.as_ref() { fs::create_dir_all(base_storage_directory.join(parsed_relay_spec.id())).unwrap(); } let cfg = smoldot_full_node::ChainConfig { chain_spec: spec_json.into(), additional_bootnodes: Vec::new(), keystore_memory: Vec::new(), sqlite_database_path: base_storage_directory.as_ref().map(|d| { d.join(parsed_relay_spec.id()) .join("database") .join("database.sqlite") }), sqlite_cache_size: cli_options.relay_chain_database_cache_size.0, keystore_path: base_storage_directory .as_ref() .map(|path| path.join(parsed_relay_spec.id()).join("keys")), json_rpc_listen: None, }; (Some(cfg), Some(relay_chain_name.to_owned())) } else { (None, None) }; // Determine which networking key to use. // // This is either passed as a CLI option, loaded from disk, or generated randomly. // TODO: move this code to `/lib/src/identity`? let libp2p_key = if let Some(node_key) = cli_options.libp2p_key { node_key } else if let Some(dir) = base_storage_directory.as_ref() { let path = dir.join("libp2p_ed25519_secret_key.secret"); let libp2p_key = if path.exists() { let file_content = zeroize::Zeroizing::new( fs::read_to_string(&path).expect("failed to read libp2p secret key file content"), ); let mut hex_decoded = Box::new([0u8; 32]); hex::decode_to_slice(file_content, &mut *hex_decoded) .expect("invalid libp2p secret key file content"); hex_decoded } else { let mut actual_key = Box::new([0u8; 32]); rand::Fill::try_fill(&mut *actual_key, &mut rand::thread_rng()).unwrap(); let mut hex_encoded = Box::new([0; 64]); hex::encode_to_slice(*actual_key, &mut *hex_encoded).unwrap(); fs::write(&path, *hex_encoded).expect("failed to write libp2p secret key file"); zeroize::Zeroize::zeroize(&mut *hex_encoded); actual_key }; // On Unix platforms, set the permission as 0o400 (only reading and by owner is permitted). // TODO: do something equivalent on Windows #[cfg(unix)] let _ = fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o400)); libp2p_key } else { let mut key = Box::new([0u8; 32]); rand::Fill::try_fill(&mut *key, &mut rand::thread_rng()).unwrap(); key }; // Create an executor where tasks are going to be spawned onto. let executor = Arc::new(smol::Executor::new()); for n in 0..thread::available_parallelism() .map(|n| n.get() - 1) .unwrap_or(3) { let executor = executor.clone(); let spawn_result = thread::Builder::new() .name(format!("tasks-pool-{}", n)) .spawn(move || smol::block_on(executor.run(smol::future::pending::<()>()))); // Ignore a failure to spawn a thread, as we're going to run tasks on the current thread // later down this function. if let Err(err) = spawn_result { log_callback.log( smoldot_full_node::LogLevel::Warn, format!("tasks-pool-thread-spawn-failure; err={err}"), ); } } // Print some general information. log_callback.log( smoldot_full_node::LogLevel::Info, "smoldot full node".to_string(), ); log_callback.log( smoldot_full_node::LogLevel::Info, "Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.".to_string(), ); log_callback.log( smoldot_full_node::LogLevel::Info, "Copyright (C) 2023 Pierre Krieger.".to_string(), ); log_callback.log( smoldot_full_node::LogLevel::Info, "This program comes with ABSOLUTELY NO WARRANTY.".to_string(), ); log_callback.log( smoldot_full_node::LogLevel::Info, "This is free software, and you are welcome to redistribute it under certain conditions." .to_string(), ); // This warning message should be removed if/when the full node becomes mature. log_callback.log( smoldot_full_node::LogLevel::Warn, "Please note that this full node is experimental. It is not feature complete and is \ known to panic often. Please report any panic you might encounter to \ ." .to_string(), ); let client_init_result = smoldot_full_node::start(smoldot_full_node::Config { chain: smoldot_full_node::ChainConfig { chain_spec: chain_spec.into(), additional_bootnodes: cli_options .additional_bootnode .iter() .map(|cli::Bootnode { address, peer_id }| (peer_id.clone(), address.clone())) .collect(), keystore_memory: cli_options.keystore_memory, sqlite_database_path, sqlite_cache_size: cli_options.database_cache_size.0, keystore_path, json_rpc_listen: if let Some(address) = cli_options.json_rpc_address.0 { Some(smoldot_full_node::JsonRpcListenConfig { address, max_json_rpc_clients: cli_options.json_rpc_max_clients, }) } else { None }, }, relay_chain, libp2p_key, listen_addresses: cli_options.listen_addr, tasks_executor: { let executor = executor.clone(); Arc::new(move |task| executor.spawn(task).detach()) }, log_callback: log_callback.clone(), jaeger_agent: cli_options.jaeger, }) .await; let client = match client_init_result { Ok(c) => c, Err(err) => { log_callback.log( smoldot_full_node::LogLevel::Error, format!("Failed to initialize client: {}", err), ); panic!("Failed to initialize client: {}", err); } }; if let Some(addr) = client.json_rpc_server_addr() { log_callback.log( smoldot_full_node::LogLevel::Info, format!( "JSON-RPC server listening on {addr}. Visit \ in order to \ interact with the node." ), ); } // Starting from here, a SIGINT (or equivalent) handler is set up. If the user does Ctrl+C, // an event will be triggered on `ctrlc_detected`. // This should be performed after all the expensive initialization is done, as otherwise these // expensive initializations aren't interrupted by Ctrl+C, which could be frustrating for the // user. let ctrlc_detected = { let event = event_listener::Event::new(); let listen = event.listen(); if let Err(err) = ctrlc::set_handler(move || { event.notify(usize::MAX); }) { // It is not critical to fail to setup the Ctrl-C handler. log_callback.log( smoldot_full_node::LogLevel::Warn, format!("ctrlc-handler-setup-fail; err={err}"), ); } listen }; // Spawn a task that prints the informant at a regular interval. // The interval is fast enough that the informant should be visible roughly at any time, // even if the terminal is filled with logs. // Note that this task also holds the smoldot `client` alive, and thus we spawn it even if // the informant is disabled. let main_task = executor.spawn({ let show_informant = matches!(cli_output, cli::Output::Informant); let informant_colors = match cli_options.color { cli::ColorChoice::Always => true, cli::ColorChoice::Never => false, }; async move { let mut informant_timer = if show_informant { smol::Timer::after(Duration::new(0, 0)) } else { smol::Timer::never() }; loop { informant_timer = smol::Timer::at(informant_timer.await + Duration::from_millis(100)); // We end the informant line with a `\r` so that it overwrites itself // every time. If any other line gets printed, it will overwrite the // informant, and the informant will then print itself below, which is // a fine behaviour. let sync_state = client.sync_state().await; eprint!( "{}\r", smoldot::informant::InformantLine { enable_colors: informant_colors, chain_name: parsed_chain_spec.name(), relay_chain: client.relay_chain_sync_state().await.map( |relay_sync_state| smoldot::informant::RelayChain { chain_name: relay_chain_name.as_ref().unwrap(), best_number: relay_sync_state.best_block_number, } ), max_line_width: terminal_size::terminal_size() .map_or(80, |(w, _)| w.0.into()), num_peers: client.num_peers().await, num_network_connections: client.num_network_connections().await, best_number: sync_state.best_block_number, finalized_number: sync_state.finalized_block_number, best_hash: &sync_state.best_block_hash, finalized_hash: &sync_state.finalized_block_hash, network_known_best: client.network_known_best().await, } ); } } }); // Now run all the tasks that have been spawned. executor.run(ctrlc_detected).await; // Add a new line after the informant so that the user's shell doesn't // overwrite it. if matches!(cli_output, cli::Output::Informant) { eprintln!(); } // After `ctrlc_detected` has triggered, we destroy `main_task`, which cancels it and destroys // the smoldot client. drop::>(main_task); // TODO: consider running the executor until all tasks shut down gracefully; unfortunately this currently hangs }