Skip to main content

forest/cli_shared/cli/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod client;
5mod completion_cmd;
6mod config;
7
8use std::{
9    net::SocketAddr,
10    path::{Path, PathBuf},
11};
12
13use crate::networks::NetworkChain;
14use crate::utils::misc::LoggingColor;
15use crate::{cli_shared::read_config, daemon::db_util::ImportMode};
16use ahash::HashSet;
17use anyhow::Context as _;
18use clap::Parser;
19use directories::ProjectDirs;
20use libp2p::Multiaddr;
21use tracing::error;
22
23pub use self::{client::*, completion_cmd::*, config::*};
24
25pub static HELP_MESSAGE: &str = "\
26{name} {version}
27{author}
28{about}
29
30USAGE:
31  {usage}
32
33SUBCOMMANDS:
34{subcommands}
35
36OPTIONS:
37{options}
38";
39
40/// CLI options
41#[derive(Default, Debug, Parser)]
42pub struct CliOpts {
43    /// A TOML file containing relevant configurations
44    #[arg(long)]
45    pub config: Option<PathBuf>,
46    /// The genesis CAR file
47    #[arg(long)]
48    pub genesis: Option<PathBuf>,
49    /// Allow RPC to be active or not (default: true)
50    #[arg(long)]
51    pub rpc: Option<bool>,
52    /// Disable Metrics endpoint
53    #[arg(long)]
54    pub no_metrics: bool,
55    /// Address used for metrics collection server. By defaults binds on
56    /// localhost on port 6116.
57    #[arg(long)]
58    pub metrics_address: Option<SocketAddr>,
59    /// Address used for RPC. By defaults binds on localhost on port 2345.
60    #[arg(long)]
61    pub rpc_address: Option<SocketAddr>,
62    /// Path to a list of RPC methods to allow/disallow.
63    #[arg(long)]
64    pub rpc_filter_list: Option<PathBuf>,
65    /// Disable healthcheck endpoints
66    #[arg(long)]
67    pub no_healthcheck: bool,
68    /// Address used for healthcheck server. By defaults binds on localhost on port 2346.
69    #[arg(long)]
70    pub healthcheck_address: Option<SocketAddr>,
71    /// P2P listen addresses, e.g., `--p2p-listen-address /ip4/0.0.0.0/tcp/12345 --p2p-listen-address /ip4/0.0.0.0/tcp/12346`
72    #[arg(long)]
73    pub p2p_listen_address: Option<Vec<Multiaddr>>,
74    /// Allow Kademlia (default: true)
75    #[arg(long)]
76    pub kademlia: Option<bool>,
77    /// Allow MDNS (default: false)
78    #[arg(long)]
79    pub mdns: Option<bool>,
80    /// Validate snapshot at given EPOCH, use a negative value -N to validate
81    /// the last N EPOCH(s) starting at HEAD.
82    #[arg(long)]
83    pub height: Option<i64>,
84    /// Sets the current HEAD epoch to validate to. Useful to specify a
85    /// smaller range in conjunction with `height`, ignored if `height`
86    /// is unspecified.
87    #[arg(long)]
88    pub head: Option<u64>,
89    /// Import a snapshot from a local CAR file or URL
90    #[arg(long)]
91    pub import_snapshot: Option<String>,
92    /// Snapshot import mode. Available modes are `auto`, `copy`, `move`, `symlink` and `hardlink`.
93    #[arg(long, default_value = "auto")]
94    pub import_mode: ImportMode,
95    /// Halt with exit code 0 after successfully importing a snapshot
96    #[arg(long)]
97    pub halt_after_import: bool,
98    /// Remove the existing chain data on a snapshot-import.
99    #[arg(long)]
100    pub remove_existing_chain: bool,
101    /// Skips loading CAR file and uses header to index chain. Assumes a
102    /// pre-loaded database
103    #[arg(long)]
104    pub skip_load: Option<bool>,
105    /// Number of tipsets requested over one chain exchange (default is 8)
106    #[arg(long)]
107    pub req_window: Option<usize>,
108    /// Number of tipsets to include in the sample that determines what the
109    /// network head is (default is 5)
110    #[arg(long)]
111    pub tipset_sample_size: Option<u8>,
112    /// Amount of Peers we want to be connected to (default is 75)
113    #[arg(long)]
114    pub target_peer_count: Option<u32>,
115    /// Encrypt the key-store (default: true)
116    #[arg(long)]
117    pub encrypt_keystore: Option<bool>,
118    /// Choose network chain to sync to
119    #[arg(long)]
120    pub chain: Option<NetworkChain>,
121    /// Automatically download a chain specific snapshot to sync with the
122    /// Filecoin network if needed.
123    #[arg(long)]
124    pub auto_download_snapshot: bool,
125    /// Enable or disable colored logging in `stdout`
126    #[arg(long, default_value = "auto")]
127    pub color: LoggingColor,
128    /// Turn on tokio-console support for debugging
129    #[arg(long)]
130    pub tokio_console: bool,
131    /// Send telemetry to `grafana loki`
132    #[arg(long)]
133    pub loki: bool,
134    /// Endpoint of `grafana loki`
135    #[arg(long, default_value = "http://127.0.0.1:3100")]
136    pub loki_endpoint: String,
137    /// Specify a directory into which rolling log files should be appended
138    #[arg(long)]
139    pub log_dir: Option<PathBuf>,
140    /// Exit after basic daemon initialization
141    #[arg(long)]
142    pub exit_after_init: bool,
143    /// If provided, indicates the file to which to save the admin token.
144    #[arg(long)]
145    pub save_token: Option<PathBuf>,
146    /// Disable the automatic database garbage collection.
147    #[arg(long)]
148    pub no_gc: bool,
149    /// In stateless mode, forest connects to the P2P network but does not sync to HEAD.
150    #[arg(long)]
151    pub stateless: bool,
152    /// Check your command-line options and configuration file if one is used
153    #[arg(long)]
154    pub dry_run: bool,
155    /// Skip loading actors from the actors bundle.
156    #[arg(long)]
157    pub skip_load_actors: bool,
158}
159
160impl CliOpts {
161    pub fn to_config(&self) -> anyhow::Result<(Config, Option<ConfigPath>)> {
162        let (path, mut cfg) = read_config(self.config.as_ref(), self.chain.clone())?;
163
164        if let Some(genesis_file) = &self.genesis {
165            cfg.client.genesis_file = Some(genesis_file.to_owned());
166        }
167        if self.rpc.unwrap_or(cfg.client.enable_rpc) {
168            cfg.client.enable_rpc = true;
169            cfg.client.rpc_filter_list = self.rpc_filter_list.clone();
170            if let Some(rpc_address) = self.rpc_address {
171                cfg.client.rpc_address = rpc_address;
172            }
173        } else {
174            cfg.client.enable_rpc = false;
175        }
176
177        if self.no_healthcheck {
178            cfg.client.enable_health_check = false;
179        } else {
180            cfg.client.enable_health_check = true;
181            if let Some(healthcheck_address) = self.healthcheck_address {
182                cfg.client.healthcheck_address = healthcheck_address;
183            }
184        }
185
186        if self.no_metrics {
187            cfg.client.enable_metrics_endpoint = false;
188        } else {
189            cfg.client.enable_metrics_endpoint = true;
190            if let Some(metrics_address) = self.metrics_address {
191                cfg.client.metrics_address = metrics_address;
192            }
193        }
194
195        if let Some(addresses) = &self.p2p_listen_address {
196            cfg.network.listening_multiaddrs.clone_from(addresses);
197        }
198
199        if let Some(snapshot_path) = &self.import_snapshot {
200            cfg.client.snapshot_path = Some(snapshot_path.into());
201            cfg.client.import_mode = self.import_mode;
202        }
203
204        cfg.client.snapshot_height = self.height;
205        cfg.client.snapshot_head = self.head.map(|head| head as i64);
206        if let Some(skip_load) = self.skip_load {
207            cfg.client.skip_load = skip_load;
208        }
209
210        cfg.network.kademlia = self.kademlia.unwrap_or(cfg.network.kademlia);
211        cfg.network.mdns = self.mdns.unwrap_or(cfg.network.mdns);
212        if let Some(target_peer_count) = self.target_peer_count {
213            cfg.network.target_peer_count = target_peer_count;
214        }
215        // (where to find these flags, should be easy to do with structops)
216
217        if let Some(encrypt_keystore) = self.encrypt_keystore {
218            cfg.client.encrypt_keystore = encrypt_keystore;
219        }
220
221        cfg.client.load_actors = !self.skip_load_actors;
222
223        Ok((cfg, path))
224    }
225}
226
227/// CLI RPC options
228#[derive(Default, Debug, Parser)]
229pub struct CliRpcOpts {
230    /// Admin token to interact with the node
231    #[arg(long)]
232    pub token: Option<String>,
233}
234
235#[derive(Debug, PartialEq)]
236pub enum ConfigPath {
237    Cli(PathBuf),
238    Env(PathBuf),
239    Project(PathBuf),
240}
241
242impl ConfigPath {
243    pub fn to_path_buf(&self) -> &PathBuf {
244        match self {
245            ConfigPath::Cli(path) => path,
246            ConfigPath::Env(path) => path,
247            ConfigPath::Project(path) => path,
248        }
249    }
250}
251
252pub fn find_config_path(config: Option<&PathBuf>) -> Option<ConfigPath> {
253    if let Some(s) = config {
254        return Some(ConfigPath::Cli(s.to_owned()));
255    }
256    if let Ok(s) = std::env::var("FOREST_CONFIG_PATH") {
257        return Some(ConfigPath::Env(PathBuf::from(s)));
258    }
259    if let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest") {
260        let path = dir.config_dir().join("config.toml");
261        if path.exists() {
262            return Some(ConfigPath::Project(path));
263        }
264    }
265    None
266}
267
268fn find_unknown_keys<'a>(
269    tables: Vec<&'a str>,
270    x: &'a toml::Value,
271    y: &'a toml::Value,
272    result: &mut Vec<(Vec<&'a str>, &'a str)>,
273) {
274    if let (toml::Value::Table(x_map), toml::Value::Table(y_map)) = (x, y) {
275        let x_set: HashSet<_> = x_map.keys().collect();
276        let y_set: HashSet<_> = y_map.keys().collect();
277        for k in x_set.difference(&y_set) {
278            result.push((tables.clone(), k));
279        }
280        for (x_key, x_value) in x_map.iter() {
281            if let Some(y_value) = y_map.get(x_key) {
282                let mut copy = tables.clone();
283                copy.push(x_key);
284                find_unknown_keys(copy, x_value, y_value, result);
285            }
286        }
287    }
288    if let (toml::Value::Array(x_vec), toml::Value::Array(y_vec)) = (x, y) {
289        for (x_value, y_value) in x_vec.iter().zip(y_vec.iter()) {
290            find_unknown_keys(tables.clone(), x_value, y_value, result);
291        }
292    }
293}
294
295pub fn check_for_unknown_keys(path: &Path, config: &Config) -> anyhow::Result<()> {
296    let file = std::fs::read_to_string(path)
297        .with_context(|| format!("failed to read config file {}", path.display()))?;
298    let value = toml::Value::Table(
299        file.parse::<toml::Table>()
300            .with_context(|| format!("config file {} is not valid TOML", path.display()))?,
301    );
302
303    let config_file = toml::to_string(config).context("failed to serialize config")?;
304    let config_value = toml::Value::Table(
305        config_file
306            .parse::<toml::Table>()
307            .context("failed to parse serialized config")?,
308    );
309
310    let mut result = vec![];
311    find_unknown_keys(vec![], &value, &config_value, &mut result);
312    for (tables, k) in result.iter() {
313        if tables.is_empty() {
314            error!("Unknown key `{k}` in top-level table");
315        } else {
316            error!("Unknown key `{k}` in [{}]", tables.join("."));
317        }
318    }
319    if !result.is_empty() {
320        let path = path.display();
321        cli_error_and_die(
322            format!("Error checking {path}. Verify that all keys are valid"),
323            1,
324        )
325    }
326    Ok(())
327}
328
329/// Print an error message and exit the program with an error code
330/// Used for handling high level errors such as invalid parameters
331pub fn cli_error_and_die(msg: impl AsRef<str>, code: i32) -> ! {
332    error!("{}", msg.as_ref());
333    std::process::exit(code);
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_check_for_unknown_keys() {
342        let config = Config::default();
343        let config_content = toml::to_string(&config).unwrap();
344        let temp_file = tempfile::Builder::new().tempfile().unwrap();
345        std::fs::write(temp_file.path(), config_content).unwrap();
346        check_for_unknown_keys(temp_file.path(), &config).unwrap();
347    }
348
349    #[test]
350    fn find_unknown_keys_must_work() {
351        let x: toml::Value = toml::from_str(
352            r#"
353            folklore = true
354            foo = "foo"
355            [myth]
356            author = 'H. P. Lovecraft'
357            entities = [
358                { name = 'Cthulhu' },
359                { name = 'Azathoth' },
360                { baz = 'Dagon' },
361            ]
362            bar = "bar"
363        "#,
364        )
365        .unwrap();
366
367        let y: toml::Value = toml::from_str(
368            r#"
369            folklore = true
370            [myth]
371            author = 'H. P. Lovecraft'
372            entities = [
373                { name = 'Cthulhu' },
374                { name = 'Azathoth' },
375                { name = 'Dagon' },
376            ]
377        "#,
378        )
379        .unwrap();
380
381        // No differences
382        let mut result = vec![];
383        find_unknown_keys(vec![], &y, &y, &mut result);
384        assert!(result.is_empty());
385
386        // 3 unknown keys
387        let mut result = vec![];
388        find_unknown_keys(vec![], &x, &y, &mut result);
389        assert_eq!(
390            result,
391            vec![
392                (vec![], "foo"),
393                (vec!["myth"], "bar"),
394                (vec!["myth", "entities"], "baz"),
395            ]
396        );
397    }
398
399    #[test]
400    fn combination_of_import_snapshot_and_import_chain_should_fail() {
401        // Creating a config with default cli options should succeed
402        let options = CliOpts::default();
403        assert!(options.to_config().is_ok());
404
405        // Creating a config with only --import_snapshot should succeed
406        let options = CliOpts {
407            import_snapshot: Some("snapshot.car".into()),
408            ..Default::default()
409        };
410        assert!(options.to_config().is_ok());
411    }
412}