mod client;
mod config;
use std::{
net::SocketAddr,
path::{Path, PathBuf},
};
use crate::networks::NetworkChain;
use crate::utils::misc::LoggingColor;
use crate::{cli_shared::read_config, daemon::db_util::ImportMode};
use ahash::HashSet;
use clap::Parser;
use directories::ProjectDirs;
use libp2p::Multiaddr;
use tracing::error;
pub use self::{client::*, config::*};
pub static HELP_MESSAGE: &str = "\
{name} {version}
{author}
{about}
USAGE:
{usage}
SUBCOMMANDS:
{subcommands}
OPTIONS:
{options}
";
#[derive(Default, Debug, Parser)]
pub struct CliOpts {
#[arg(long)]
pub config: Option<PathBuf>,
#[arg(long)]
pub genesis: Option<PathBuf>,
#[arg(long)]
pub rpc: Option<bool>,
#[arg(long)]
pub no_metrics: bool,
#[arg(long)]
pub metrics_address: Option<SocketAddr>,
#[arg(long)]
pub rpc_address: Option<SocketAddr>,
#[arg(long)]
pub no_healthcheck: bool,
#[arg(long)]
pub healthcheck_address: Option<SocketAddr>,
#[arg(long)]
pub p2p_listen_address: Option<Vec<Multiaddr>>,
#[arg(long)]
pub kademlia: Option<bool>,
#[arg(long)]
pub mdns: Option<bool>,
#[arg(long)]
pub height: Option<i64>,
#[arg(long)]
pub head: Option<u64>,
#[arg(long)]
pub import_snapshot: Option<String>,
#[arg(long, default_value = "auto")]
pub import_mode: ImportMode,
#[arg(long)]
pub halt_after_import: bool,
#[arg(long)]
pub skip_load: Option<bool>,
#[arg(long)]
pub req_window: Option<usize>,
#[arg(long)]
pub tipset_sample_size: Option<u8>,
#[arg(long)]
pub target_peer_count: Option<u32>,
#[arg(long)]
pub encrypt_keystore: Option<bool>,
#[arg(long)]
pub chain: Option<NetworkChain>,
#[arg(long)]
pub detach: bool,
#[arg(long)]
pub auto_download_snapshot: bool,
#[arg(long, default_value = "auto")]
pub color: LoggingColor,
#[arg(long)]
pub tokio_console: bool,
#[arg(long)]
pub loki: bool,
#[arg(long, default_value = "http://127.0.0.1:3100")]
pub loki_endpoint: String,
#[arg(long)]
pub log_dir: Option<PathBuf>,
#[arg(long)]
pub exit_after_init: bool,
#[arg(long)]
pub save_token: Option<PathBuf>,
#[arg(long)]
pub track_peak_rss: bool,
#[arg(long)]
pub no_gc: bool,
#[arg(long)]
pub stateless: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub skip_load_actors: bool,
}
impl CliOpts {
pub fn to_config(&self) -> Result<(Config, Option<ConfigPath>), anyhow::Error> {
let (path, mut cfg) = read_config(self.config.as_ref(), self.chain.clone())?;
if let Some(genesis_file) = &self.genesis {
cfg.client.genesis_file = Some(genesis_file.to_owned());
}
if self.rpc.unwrap_or(cfg.client.enable_rpc) {
cfg.client.enable_rpc = true;
if let Some(rpc_address) = self.rpc_address {
cfg.client.rpc_address = rpc_address;
}
} else {
cfg.client.enable_rpc = false;
}
if self.no_healthcheck {
cfg.client.enable_health_check = false;
} else {
cfg.client.enable_health_check = true;
if let Some(healthcheck_address) = self.healthcheck_address {
cfg.client.healthcheck_address = healthcheck_address;
}
}
if self.no_metrics {
cfg.client.enable_metrics_endpoint = false;
} else {
cfg.client.enable_metrics_endpoint = true;
if let Some(metrics_address) = self.metrics_address {
cfg.client.metrics_address = metrics_address;
}
}
if let Some(addresses) = &self.p2p_listen_address {
cfg.network.listening_multiaddrs.clone_from(addresses);
}
if let Some(snapshot_path) = &self.import_snapshot {
cfg.client.snapshot_path = Some(snapshot_path.into());
cfg.client.import_mode = self.import_mode;
}
cfg.client.snapshot_height = self.height;
cfg.client.snapshot_head = self.head.map(|head| head as i64);
if let Some(skip_load) = self.skip_load {
cfg.client.skip_load = skip_load;
}
cfg.network.kademlia = self.kademlia.unwrap_or(cfg.network.kademlia);
cfg.network.mdns = self.mdns.unwrap_or(cfg.network.mdns);
if let Some(target_peer_count) = self.target_peer_count {
cfg.network.target_peer_count = target_peer_count;
}
if let Some(req_window) = self.req_window {
cfg.sync.request_window = req_window;
}
if let Some(tipset_sample_size) = self.tipset_sample_size {
cfg.sync.tipset_sample_size = tipset_sample_size.into();
}
if let Some(encrypt_keystore) = self.encrypt_keystore {
cfg.client.encrypt_keystore = encrypt_keystore;
}
cfg.client.load_actors = !self.skip_load_actors;
Ok((cfg, path))
}
}
#[derive(Default, Debug, Parser)]
pub struct CliRpcOpts {
#[arg(long)]
pub token: Option<String>,
}
#[derive(Debug, PartialEq)]
pub enum ConfigPath {
Cli(PathBuf),
Env(PathBuf),
Project(PathBuf),
}
impl ConfigPath {
pub fn to_path_buf(&self) -> &PathBuf {
match self {
ConfigPath::Cli(path) => path,
ConfigPath::Env(path) => path,
ConfigPath::Project(path) => path,
}
}
}
pub fn find_config_path(config: Option<&PathBuf>) -> Option<ConfigPath> {
if let Some(s) = config {
return Some(ConfigPath::Cli(s.to_owned()));
}
if let Ok(s) = std::env::var("FOREST_CONFIG_PATH") {
return Some(ConfigPath::Env(PathBuf::from(s)));
}
if let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest") {
let path = dir.config_dir().join("config.toml");
if path.exists() {
return Some(ConfigPath::Project(path));
}
}
None
}
fn find_unknown_keys<'a>(
tables: Vec<&'a str>,
x: &'a toml::Value,
y: &'a toml::Value,
result: &mut Vec<(Vec<&'a str>, &'a str)>,
) {
if let (toml::Value::Table(x_map), toml::Value::Table(y_map)) = (x, y) {
let x_set: HashSet<_> = x_map.keys().collect();
let y_set: HashSet<_> = y_map.keys().collect();
for k in x_set.difference(&y_set) {
result.push((tables.clone(), k));
}
for (x_key, x_value) in x_map.iter() {
if let Some(y_value) = y_map.get(x_key) {
let mut copy = tables.clone();
copy.push(x_key);
find_unknown_keys(copy, x_value, y_value, result);
}
}
}
if let (toml::Value::Array(x_vec), toml::Value::Array(y_vec)) = (x, y) {
for (x_value, y_value) in x_vec.iter().zip(y_vec.iter()) {
find_unknown_keys(tables.clone(), x_value, y_value, result);
}
}
}
pub fn check_for_unknown_keys(path: &Path, config: &Config) {
let file = std::fs::read_to_string(path).unwrap();
let value = file.parse::<toml::Value>().unwrap();
let config_file = toml::to_string(config).unwrap();
let config_value = config_file.parse::<toml::Value>().unwrap();
let mut result = vec![];
find_unknown_keys(vec![], &value, &config_value, &mut result);
for (tables, k) in result.iter() {
if tables.is_empty() {
error!("Unknown key `{k}` in top-level table");
} else {
error!("Unknown key `{k}` in [{}]", tables.join("."));
}
}
if !result.is_empty() {
let path = path.display();
cli_error_and_die(
format!("Error checking {path}. Verify that all keys are valid"),
1,
)
}
}
pub fn cli_error_and_die(msg: impl AsRef<str>, code: i32) -> ! {
error!("{}", msg.as_ref());
std::process::exit(code);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_unknown_keys_must_work() {
let x: toml::Value = toml::from_str(
r#"
folklore = true
foo = "foo"
[myth]
author = 'H. P. Lovecraft'
entities = [
{ name = 'Cthulhu' },
{ name = 'Azathoth' },
{ baz = 'Dagon' },
]
bar = "bar"
"#,
)
.unwrap();
let y: toml::Value = toml::from_str(
r#"
folklore = true
[myth]
author = 'H. P. Lovecraft'
entities = [
{ name = 'Cthulhu' },
{ name = 'Azathoth' },
{ name = 'Dagon' },
]
"#,
)
.unwrap();
let mut result = vec![];
find_unknown_keys(vec![], &y, &y, &mut result);
assert!(result.is_empty());
let mut result = vec![];
find_unknown_keys(vec![], &x, &y, &mut result);
assert_eq!(
result,
vec![
(vec![], "foo"),
(vec!["myth"], "bar"),
(vec!["myth", "entities"], "baz"),
]
);
}
#[test]
fn combination_of_import_snapshot_and_import_chain_should_fail() {
let options = CliOpts::default();
assert!(options.to_config().is_ok());
let options = CliOpts {
import_snapshot: Some("snapshot.car".into()),
..Default::default()
};
assert!(options.to_config().is_ok());
}
}