// 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
}