1mod 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#[derive(Default, Debug, Parser)]
42pub struct CliOpts {
43 #[arg(long)]
45 pub config: Option<PathBuf>,
46 #[arg(long)]
48 pub genesis: Option<PathBuf>,
49 #[arg(long)]
51 pub rpc: Option<bool>,
52 #[arg(long)]
54 pub no_metrics: bool,
55 #[arg(long)]
58 pub metrics_address: Option<SocketAddr>,
59 #[arg(long)]
61 pub rpc_address: Option<SocketAddr>,
62 #[arg(long)]
64 pub rpc_filter_list: Option<PathBuf>,
65 #[arg(long)]
67 pub no_healthcheck: bool,
68 #[arg(long)]
70 pub healthcheck_address: Option<SocketAddr>,
71 #[arg(long)]
73 pub p2p_listen_address: Option<Vec<Multiaddr>>,
74 #[arg(long)]
76 pub kademlia: Option<bool>,
77 #[arg(long)]
79 pub mdns: Option<bool>,
80 #[arg(long)]
83 pub height: Option<i64>,
84 #[arg(long)]
88 pub head: Option<u64>,
89 #[arg(long)]
91 pub import_snapshot: Option<String>,
92 #[arg(long, default_value = "auto")]
94 pub import_mode: ImportMode,
95 #[arg(long)]
97 pub halt_after_import: bool,
98 #[arg(long)]
100 pub remove_existing_chain: bool,
101 #[arg(long)]
104 pub skip_load: Option<bool>,
105 #[arg(long)]
107 pub req_window: Option<usize>,
108 #[arg(long)]
111 pub tipset_sample_size: Option<u8>,
112 #[arg(long)]
114 pub target_peer_count: Option<u32>,
115 #[arg(long)]
117 pub encrypt_keystore: Option<bool>,
118 #[arg(long)]
120 pub chain: Option<NetworkChain>,
121 #[arg(long)]
124 pub auto_download_snapshot: bool,
125 #[arg(long, default_value = "auto")]
127 pub color: LoggingColor,
128 #[arg(long)]
130 pub tokio_console: bool,
131 #[arg(long)]
133 pub loki: bool,
134 #[arg(long, default_value = "http://127.0.0.1:3100")]
136 pub loki_endpoint: String,
137 #[arg(long)]
139 pub log_dir: Option<PathBuf>,
140 #[arg(long)]
142 pub exit_after_init: bool,
143 #[arg(long)]
145 pub save_token: Option<PathBuf>,
146 #[arg(long)]
148 pub no_gc: bool,
149 #[arg(long)]
151 pub stateless: bool,
152 #[arg(long)]
154 pub dry_run: bool,
155 #[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.clone_from(&self.rpc_filter_list);
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 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#[derive(Default, Debug, Parser)]
229pub struct CliRpcOpts {
230 #[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
329pub 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 let mut result = vec![];
383 find_unknown_keys(vec![], &y, &y, &mut result);
384 assert!(result.is_empty());
385
386 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 let options = CliOpts::default();
403 assert!(options.to_config().is_ok());
404
405 let options = CliOpts {
407 import_snapshot: Some("snapshot.car".into()),
408 ..Default::default()
409 };
410 assert!(options.to_config().is_ok());
411 }
412}