use clap::Parser;
use fuel_core::service::genesis::NotifyCancel;
use fuel_core_chain_config::{
ChainConfig,
SnapshotReader,
StateConfig,
};
use std::{
env,
path::PathBuf,
str::FromStr,
};
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{
filter::EnvFilter,
layer::SubscriberExt,
registry,
Layer,
};
#[cfg(feature = "env")]
use dotenvy::dotenv;
pub fn default_db_path() -> PathBuf {
dirs::home_dir().unwrap().join(".fuel").join("db")
}
pub mod fee_contract;
#[cfg(feature = "rocksdb")]
pub mod rollback;
pub mod run;
#[cfg(feature = "rocksdb")]
pub mod snapshot;
pub const DEFAULT_DATABASE_CACHE_SIZE: usize = 1024 * 1024 * 1024;
#[derive(Parser, Debug)]
#[clap(
name = "fuel-core",
about = "Fuel client implementation",
version,
rename_all = "kebab-case"
)]
pub struct Opt {
#[clap(subcommand)]
command: Fuel,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Parser)]
pub enum Fuel {
Run(run::Command),
#[cfg(feature = "rocksdb")]
Snapshot(snapshot::Command),
#[cfg(feature = "rocksdb")]
Rollback(rollback::Command),
GenerateFeeContract(fee_contract::Command),
}
pub const LOG_FILTER: &str = "RUST_LOG";
pub const HUMAN_LOGGING: &str = "HUMAN_LOGGING";
#[cfg(feature = "env")]
fn init_environment() -> Option<PathBuf> {
dotenv().ok()
}
#[cfg(not(feature = "env"))]
fn init_environment() -> Option<PathBuf> {
None
}
pub fn init_logging() {
let filter = match env::var_os(LOG_FILTER) {
Some(_) => {
EnvFilter::try_from_default_env().expect("Invalid `RUST_LOG` provided")
}
None => EnvFilter::new("info"),
};
let human_logging = env::var_os(HUMAN_LOGGING)
.map(|s| {
bool::from_str(s.to_str().unwrap())
.expect("Expected `true` or `false` to be provided for `HUMAN_LOGGING`")
})
.unwrap_or(true);
let layer = tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr);
let fmt = if human_logging {
layer
.with_ansi(true)
.with_level(true)
.with_line_number(true)
.boxed()
} else {
layer
.with_ansi(false)
.with_level(true)
.with_line_number(true)
.json()
.boxed()
};
let subscriber = registry::Registry::default() .with(filter) .with(fmt);
tracing::subscriber::set_global_default(subscriber)
.expect("setting global default failed");
}
pub async fn run_cli() -> anyhow::Result<()> {
init_logging();
if let Some(path) = init_environment() {
let path = path.display();
tracing::info!("Loading environment variables from {path}");
}
let opt = Opt::try_parse();
if opt.is_err() {
let command = run::Command::try_parse();
if let Ok(command) = command {
tracing::warn!("This cli format for running `fuel-core` is deprecated and will be removed. Please use `fuel-core run` or use `--help` for more information");
return run::exec(command).await;
}
}
match opt {
Ok(opt) => match opt.command {
Fuel::Run(command) => run::exec(command).await,
#[cfg(feature = "rocksdb")]
Fuel::Snapshot(command) => snapshot::exec(command).await,
Fuel::GenerateFeeContract(command) => fee_contract::exec(command).await,
Fuel::Rollback(command) => rollback::exec(command).await,
},
Err(e) => {
e.exit()
}
}
}
pub fn local_testnet_chain_config() -> ChainConfig {
const TESTNET_CHAIN_CONFIG: &[u8] =
include_bytes!("../chainspec/local-testnet/chain_config.json");
const TESTNET_CHAIN_CONFIG_STATE_BYTECODE: &[u8] =
include_bytes!("../chainspec/local-testnet/state_transition_bytecode.wasm");
let mut config: ChainConfig = serde_json::from_slice(TESTNET_CHAIN_CONFIG).unwrap();
config.state_transition_bytecode = TESTNET_CHAIN_CONFIG_STATE_BYTECODE.to_vec();
config
}
pub fn local_testnet_reader() -> SnapshotReader {
const TESTNET_STATE_CONFIG: &[u8] =
include_bytes!("../chainspec/local-testnet/state_config.json");
let state_config: StateConfig = serde_json::from_slice(TESTNET_STATE_CONFIG).unwrap();
SnapshotReader::new_in_memory(local_testnet_chain_config(), state_config)
}
#[derive(Clone)]
pub struct ShutdownListener {
token: CancellationToken,
}
impl ShutdownListener {
pub fn spawn() -> Self {
let token = CancellationToken::new();
{
let token = token.clone();
tokio::spawn(async move {
let mut sigterm = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;
let mut sigint = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::interrupt(),
)?;
#[cfg(unix)]
tokio::select! {
_ = sigterm.recv() => {
tracing::info!("Received SIGTERM");
}
_ = sigint.recv() => {
tracing::info!("Received SIGINT");
}
}
#[cfg(not(unix))]
{
tokio::signal::ctrl_c().await?;
tracing::info!("Received ctrl_c");
}
token.cancel();
tokio::io::Result::Ok(())
});
}
Self { token }
}
}
#[async_trait::async_trait]
impl NotifyCancel for ShutdownListener {
async fn wait_until_cancelled(&self) -> anyhow::Result<()> {
self.token.cancelled().await;
Ok(())
}
fn is_cancelled(&self) -> bool {
self.token.is_cancelled()
}
}
#[cfg(feature = "rocksdb")]
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use clap::Parser;
use fuel_core_types::fuel_types::ContractId;
use std::path::PathBuf;
use crate::cli::{
snapshot,
Fuel,
};
use super::Opt;
fn parse_cli(line: &str, suffix: &str) -> anyhow::Result<Opt> {
let words = line
.split_ascii_whitespace()
.chain(suffix.split_ascii_whitespace());
Opt::try_parse_from(words).map_err(|e| anyhow!(e.to_string()))
}
mod snapshot_tests {
use crate::cli::default_db_path;
use super::*;
#[test]
fn can_snapshot() {
let line = "./core snapshot";
let irrelevant_remainder = "--output-directory dir everything encoding json";
let command = parse_cli(line, irrelevant_remainder)
.expect("should parse the snapshot command")
.command;
assert!(matches!(command, Fuel::Snapshot(_)));
}
#[test]
fn db_is_default_if_not_given() {
let line = "./core snapshot";
let irrelevant_remainder = "--output-directory dir everything encoding json";
let command = parse_cli(line, irrelevant_remainder)
.expect("should parse the snapshot command")
.command;
let Fuel::Snapshot(snapshot::Command { database_path, .. }) = command else {
panic!("Expected a snapshot command")
};
assert_eq!(database_path, default_db_path().as_path());
}
#[test]
fn db_is_as_given() {
let line = "./core snapshot --db-path ./some/path";
let irrelevant_remainder = "--output-directory dir everything encoding json";
let command = parse_cli(line, irrelevant_remainder)
.expect("should parse the snapshot command")
.command;
let Fuel::Snapshot(snapshot::Command { database_path, .. }) = command else {
panic!("Expected a snapshot command")
};
assert_eq!(database_path, PathBuf::from("./some/path"));
}
#[test]
fn output_dir_required() {
let line = "./core snapshot";
let irrelevant_remainder = "everything encoding json";
let result = parse_cli(line, irrelevant_remainder);
assert!(result.is_err());
}
#[test]
fn output_dir_is_as_given() {
let line = "./core snapshot --output-directory ./some/path";
let irrelevant_remainder = "everything encoding json";
let command = parse_cli(line, irrelevant_remainder)
.expect("should parse the snapshot command")
.command;
let Fuel::Snapshot(snapshot::Command { output_dir, .. }) = command else {
panic!("Expected a snapshot command")
};
assert_eq!(output_dir, PathBuf::from("./some/path"));
}
}
mod snapshot_everything_tests {
use anyhow::bail;
use super::*;
fn extract_everything_command(
command: Fuel,
) -> anyhow::Result<(Option<PathBuf>, PathBuf, Option<snapshot::EncodingCommand>)>
{
match command {
Fuel::Snapshot(snapshot::Command {
subcommand:
snapshot::SubCommands::Everything {
chain_config,
encoding_command,
},
output_dir,
..
}) => Ok((chain_config, output_dir, encoding_command)),
_ => bail!("Expected a snapshot everything command"),
}
}
#[test]
fn snapshot_everything() {
let line = "./core snapshot --output-directory dir everything ";
let irrelevant_remainder = "encoding json";
let command = parse_cli(line, irrelevant_remainder)
.expect("should parse the snapshot command")
.command;
extract_everything_command(command).expect("Can extract command");
}
#[test]
fn output_dir_required() {
let line = "./core snapshot everything";
let result = parse_cli(line, "");
assert!(result.is_err());
}
#[test]
fn chain_config_is_as_given() {
let line =
"./core snapshot --output-directory ./some/path everything --chain ./some/chain/config";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (chain_config, _, _) =
extract_everything_command(command).expect("Can extract command");
assert_eq!(chain_config, Some(PathBuf::from("./some/chain/config")));
}
#[test]
fn encoding_is_optional() {
let line = "./core snapshot --output-directory ./some/path everything";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
assert!(encoding_command.is_none());
}
#[test]
fn chain_config_dir_is_optional() {
let line =
"./core snapshot --output-directory ./some/path everything encoding json";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (chain_config, _, _) =
extract_everything_command(command).expect("Can extract command");
assert!(chain_config.is_none());
}
#[test]
fn can_choose_json_encoding() {
let line = "./core snapshot --output-directory dir everything encoding json";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
let Some(snapshot::EncodingCommand::Encoding {
encoding: snapshot::Encoding::Json,
}) = encoding_command
else {
panic!("Expected a snapshot everything command with json encoding");
};
}
#[cfg(feature = "parquet")]
#[test]
fn can_choose_parquet_encoding() {
let line =
"./core snapshot --output-directory dir everything encoding parquet";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
let Some(snapshot::EncodingCommand::Encoding {
encoding: snapshot::Encoding::Parquet { .. },
}) = encoding_command
else {
panic!("Expected a snapshot everything command with parquet encoding");
};
}
#[cfg(feature = "parquet")]
#[test]
fn group_size_is_configurable() {
let line =
"./core snapshot --output-directory dir everything encoding parquet --group-size 101";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
let Some(snapshot::EncodingCommand::Encoding {
encoding: snapshot::Encoding::Parquet { group_size, .. },
}) = encoding_command
else {
panic!("Expected a snapshot everything command with parquet encoding");
};
assert_eq!(group_size, 101);
}
#[cfg(feature = "parquet")]
#[test]
fn group_size_has_a_default() {
let line =
"./core snapshot --output-directory dir everything encoding parquet";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
let Some(snapshot::EncodingCommand::Encoding {
encoding: snapshot::Encoding::Parquet { group_size, .. },
}) = encoding_command
else {
panic!("Expected a snapshot everything command with parquet encoding");
};
assert_eq!(group_size, 10000);
}
#[cfg(feature = "parquet")]
#[test]
fn can_configure_compression() {
let line =
"./core snapshot --output-directory dir everything encoding parquet --compression-level 7";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
let Some(snapshot::EncodingCommand::Encoding {
encoding: snapshot::Encoding::Parquet { compression, .. },
}) = encoding_command
else {
panic!("Expected a snapshot everything command with parquet encoding");
};
assert_eq!(compression, 7);
}
#[cfg(feature = "parquet")]
#[test]
fn compression_has_a_default() {
let line =
"./core snapshot --output-directory dir everything encoding parquet";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let (_, _, encoding_command) =
extract_everything_command(command).expect("Can extract command");
let Some(snapshot::EncodingCommand::Encoding {
encoding: snapshot::Encoding::Parquet { compression, .. },
}) = encoding_command
else {
panic!("Expected a snapshot everything command with parquet encoding");
};
assert_eq!(compression, 1);
}
#[test]
fn json_encoding_doesnt_allow_for_group_size() {
let line =
"./core snapshot --output-directory dir everything encoding json --group-size 101";
let result = parse_cli(line, "");
assert!(result.is_err());
}
}
mod snapshot_contract_tests {
use super::*;
#[test]
fn snapshot_contract() {
let line = "./core snapshot --output-directory ./snapshot contract";
let irrelevant_remainder =
"--id 0x0000000000000000000000000000000000000000000000000000000000000000";
let command = parse_cli(line, irrelevant_remainder)
.expect("should parse the snapshot command")
.command;
let Fuel::Snapshot(snapshot::Command {
subcommand: snapshot::SubCommands::Contract { .. },
..
}) = command
else {
panic!("Expected a snapshot contract command");
};
}
#[test]
fn snapshot_contract_id_required() {
let line = "./core snapshot --output-directory ./snapshot contract";
let result = parse_cli(line, "");
assert!(result.is_err());
}
#[test]
fn snapshot_contract_id_given() {
let line = "./core snapshot --output-directory ./snapshot contract --id 0x1111111111111111111111111111111111111111111111111111111111111111";
let command = parse_cli(line, "")
.expect("should parse the snapshot command")
.command;
let Fuel::Snapshot(snapshot::Command {
subcommand: snapshot::SubCommands::Contract { contract_id },
..
}) = command
else {
panic!("Expected a snapshot contract command");
};
assert_eq!(contract_id, ContractId::from([0x11u8; 32]));
}
}
mod run_arg_tests {
use std::path::PathBuf;
use crate::cli::run;
#[test]
fn can_ask_for_db_prune() {
let line = "./core run --db-prune";
let command = super::parse_cli(line, "")
.expect("should parse the run command")
.command;
let super::Fuel::Run(run::Command { db_prune, .. }) = command else {
panic!("Expected a run command");
};
assert!(db_prune);
}
#[test]
fn db_prune_off_by_default() {
let line = "./core run";
let command = super::parse_cli(line, "")
.expect("should parse the run command")
.command;
let super::Fuel::Run(run::Command { db_prune, .. }) = command else {
panic!("Expected a run command");
};
assert!(!db_prune);
}
#[test]
fn can_give_a_snapshot() {
let line = "./core run --snapshot ./some/path";
let command = super::parse_cli(line, "")
.expect("should parse the run command")
.command;
let super::Fuel::Run(run::Command {
snapshot: Some(snapshot),
..
}) = command
else {
panic!("Expected a run command");
};
assert_eq!(snapshot, PathBuf::from("./some/path"));
}
#[test]
fn snapshot_is_optional() {
let line = "./core run";
let command = super::parse_cli(line, "")
.expect("should parse the run command")
.command;
let super::Fuel::Run(run::Command { snapshot: None, .. }) = command else {
panic!("Expected a run command without a snapshot");
};
}
}
}