forest/cli_shared/cli/
mod.rs

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