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 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    /// Remove the existing chain data on a snapshot-import.
98    #[arg(long)]
99    pub remove_existing_chain: bool,
100    /// Skips loading CAR file and uses header to index chain. Assumes a
101    /// pre-loaded database
102    #[arg(long)]
103    pub skip_load: Option<bool>,
104    /// Number of tipsets requested over one chain exchange (default is 8)
105    #[arg(long)]
106    pub req_window: Option<usize>,
107    /// Number of tipsets to include in the sample that determines what the
108    /// network head is (default is 5)
109    #[arg(long)]
110    pub tipset_sample_size: Option<u8>,
111    /// Amount of Peers we want to be connected to (default is 75)
112    #[arg(long)]
113    pub target_peer_count: Option<u32>,
114    /// Encrypt the key-store (default: true)
115    #[arg(long)]
116    pub encrypt_keystore: Option<bool>,
117    /// Choose network chain to sync to
118    #[arg(long)]
119    pub chain: Option<NetworkChain>,
120    /// Automatically download a chain specific snapshot to sync with the
121    /// Filecoin network if needed.
122    #[arg(long)]
123    pub auto_download_snapshot: bool,
124    /// Enable or disable colored logging in `stdout`
125    #[arg(long, default_value = "auto")]
126    pub color: LoggingColor,
127    /// Turn on tokio-console support for debugging
128    #[arg(long)]
129    pub tokio_console: bool,
130    /// Send telemetry to `grafana loki`
131    #[arg(long)]
132    pub loki: bool,
133    /// Endpoint of `grafana loki`
134    #[arg(long, default_value = "http://127.0.0.1:3100")]
135    pub loki_endpoint: String,
136    /// Specify a directory into which rolling log files should be appended
137    #[arg(long)]
138    pub log_dir: Option<PathBuf>,
139    /// Exit after basic daemon initialization
140    #[arg(long)]
141    pub exit_after_init: bool,
142    /// If provided, indicates the file to which to save the admin token.
143    #[arg(long)]
144    pub save_token: Option<PathBuf>,
145    /// Disable the automatic database garbage collection.
146    #[arg(long)]
147    pub no_gc: bool,
148    /// In stateless mode, forest connects to the P2P network but does not sync to HEAD.
149    #[arg(long)]
150    pub stateless: bool,
151    /// Check your command-line options and configuration file if one is used
152    #[arg(long)]
153    pub dry_run: bool,
154    /// Skip loading actors from the actors bundle.
155    #[arg(long)]
156    pub skip_load_actors: bool,
157}
158
159impl CliOpts {
160    pub fn to_config(&self) -> anyhow::Result<(Config, Option<ConfigPath>)> {
161        let (path, mut cfg) = read_config(self.config.as_ref(), self.chain.clone())?;
162
163        if let Some(genesis_file) = &self.genesis {
164            cfg.client.genesis_file = Some(genesis_file.to_owned());
165        }
166        if self.rpc.unwrap_or(cfg.client.enable_rpc) {
167            cfg.client.enable_rpc = true;
168            cfg.client.rpc_filter_list = self.rpc_filter_list.clone();
169            if let Some(rpc_address) = self.rpc_address {
170                cfg.client.rpc_address = rpc_address;
171            }
172        } else {
173            cfg.client.enable_rpc = false;
174        }
175
176        if self.no_healthcheck {
177            cfg.client.enable_health_check = false;
178        } else {
179            cfg.client.enable_health_check = true;
180            if let Some(healthcheck_address) = self.healthcheck_address {
181                cfg.client.healthcheck_address = healthcheck_address;
182            }
183        }
184
185        if self.no_metrics {
186            cfg.client.enable_metrics_endpoint = false;
187        } else {
188            cfg.client.enable_metrics_endpoint = true;
189            if let Some(metrics_address) = self.metrics_address {
190                cfg.client.metrics_address = metrics_address;
191            }
192        }
193
194        if let Some(addresses) = &self.p2p_listen_address {
195            cfg.network.listening_multiaddrs.clone_from(addresses);
196        }
197
198        if let Some(snapshot_path) = &self.import_snapshot {
199            cfg.client.snapshot_path = Some(snapshot_path.into());
200            cfg.client.import_mode = self.import_mode;
201        }
202
203        cfg.client.snapshot_height = self.height;
204        cfg.client.snapshot_head = self.head.map(|head| head as i64);
205        if let Some(skip_load) = self.skip_load {
206            cfg.client.skip_load = skip_load;
207        }
208
209        cfg.network.kademlia = self.kademlia.unwrap_or(cfg.network.kademlia);
210        cfg.network.mdns = self.mdns.unwrap_or(cfg.network.mdns);
211        if let Some(target_peer_count) = self.target_peer_count {
212            cfg.network.target_peer_count = target_peer_count;
213        }
214        // (where to find these flags, should be easy to do with structops)
215
216        if let Some(encrypt_keystore) = self.encrypt_keystore {
217            cfg.client.encrypt_keystore = encrypt_keystore;
218        }
219
220        cfg.client.load_actors = !self.skip_load_actors;
221
222        Ok((cfg, path))
223    }
224}
225
226/// CLI RPC options
227#[derive(Default, Debug, Parser)]
228pub struct CliRpcOpts {
229    /// Admin token to interact with the node
230    #[arg(long)]
231    pub token: Option<String>,
232}
233
234#[derive(Debug, PartialEq)]
235pub enum ConfigPath {
236    Cli(PathBuf),
237    Env(PathBuf),
238    Project(PathBuf),
239}
240
241impl ConfigPath {
242    pub fn to_path_buf(&self) -> &PathBuf {
243        match self {
244            ConfigPath::Cli(path) => path,
245            ConfigPath::Env(path) => path,
246            ConfigPath::Project(path) => path,
247        }
248    }
249}
250
251pub fn find_config_path(config: Option<&PathBuf>) -> Option<ConfigPath> {
252    if let Some(s) = config {
253        return Some(ConfigPath::Cli(s.to_owned()));
254    }
255    if let Ok(s) = std::env::var("FOREST_CONFIG_PATH") {
256        return Some(ConfigPath::Env(PathBuf::from(s)));
257    }
258    if let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest") {
259        let path = dir.config_dir().join("config.toml");
260        if path.exists() {
261            return Some(ConfigPath::Project(path));
262        }
263    }
264    None
265}
266
267fn find_unknown_keys<'a>(
268    tables: Vec<&'a str>,
269    x: &'a toml::Value,
270    y: &'a toml::Value,
271    result: &mut Vec<(Vec<&'a str>, &'a str)>,
272) {
273    if let (toml::Value::Table(x_map), toml::Value::Table(y_map)) = (x, y) {
274        let x_set: HashSet<_> = x_map.keys().collect();
275        let y_set: HashSet<_> = y_map.keys().collect();
276        for k in x_set.difference(&y_set) {
277            result.push((tables.clone(), k));
278        }
279        for (x_key, x_value) in x_map.iter() {
280            if let Some(y_value) = y_map.get(x_key) {
281                let mut copy = tables.clone();
282                copy.push(x_key);
283                find_unknown_keys(copy, x_value, y_value, result);
284            }
285        }
286    }
287    if let (toml::Value::Array(x_vec), toml::Value::Array(y_vec)) = (x, y) {
288        for (x_value, y_value) in x_vec.iter().zip(y_vec.iter()) {
289            find_unknown_keys(tables.clone(), x_value, y_value, result);
290        }
291    }
292}
293
294pub fn check_for_unknown_keys(path: &Path, config: &Config) {
295    // `config` has been loaded successfully from toml file in `path` so we can
296    // always serialize it back to a valid TOML value or get the TOML value from
297    // `path`
298    let file = std::fs::read_to_string(path).unwrap();
299    let value = toml::Value::Table(file.parse::<toml::Table>().unwrap());
300
301    let config_file = toml::to_string(config).unwrap();
302    let config_value = toml::Value::Table(config_file.parse::<toml::Table>().unwrap());
303
304    let mut result = vec![];
305    find_unknown_keys(vec![], &value, &config_value, &mut result);
306    for (tables, k) in result.iter() {
307        if tables.is_empty() {
308            error!("Unknown key `{k}` in top-level table");
309        } else {
310            error!("Unknown key `{k}` in [{}]", tables.join("."));
311        }
312    }
313    if !result.is_empty() {
314        let path = path.display();
315        cli_error_and_die(
316            format!("Error checking {path}. Verify that all keys are valid"),
317            1,
318        )
319    }
320}
321
322/// Print an error message and exit the program with an error code
323/// Used for handling high level errors such as invalid parameters
324pub fn cli_error_and_die(msg: impl AsRef<str>, code: i32) -> ! {
325    error!("{}", msg.as_ref());
326    std::process::exit(code);
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_check_for_unknown_keys() {
335        let config = Config::default();
336        let config_content = toml::to_string(&config).unwrap();
337        let temp_file = tempfile::Builder::new().tempfile().unwrap();
338        std::fs::write(temp_file.path(), config_content).unwrap();
339        check_for_unknown_keys(temp_file.path(), &config);
340    }
341
342    #[test]
343    fn find_unknown_keys_must_work() {
344        let x: toml::Value = toml::from_str(
345            r#"
346            folklore = true
347            foo = "foo"
348            [myth]
349            author = 'H. P. Lovecraft'
350            entities = [
351                { name = 'Cthulhu' },
352                { name = 'Azathoth' },
353                { baz = 'Dagon' },
354            ]
355            bar = "bar"
356        "#,
357        )
358        .unwrap();
359
360        let y: toml::Value = toml::from_str(
361            r#"
362            folklore = true
363            [myth]
364            author = 'H. P. Lovecraft'
365            entities = [
366                { name = 'Cthulhu' },
367                { name = 'Azathoth' },
368                { name = 'Dagon' },
369            ]
370        "#,
371        )
372        .unwrap();
373
374        // No differences
375        let mut result = vec![];
376        find_unknown_keys(vec![], &y, &y, &mut result);
377        assert!(result.is_empty());
378
379        // 3 unknown keys
380        let mut result = vec![];
381        find_unknown_keys(vec![], &x, &y, &mut result);
382        assert_eq!(
383            result,
384            vec![
385                (vec![], "foo"),
386                (vec!["myth"], "bar"),
387                (vec!["myth", "entities"], "baz"),
388            ]
389        );
390    }
391
392    #[test]
393    fn combination_of_import_snapshot_and_import_chain_should_fail() {
394        // Creating a config with default cli options should succeed
395        let options = CliOpts::default();
396        assert!(options.to_config().is_ok());
397
398        // Creating a config with only --import_snapshot should succeed
399        let options = CliOpts {
400            import_snapshot: Some("snapshot.car".into()),
401            ..Default::default()
402        };
403        assert!(options.to_config().is_ok());
404    }
405}