1use crate::{
4 app::{run_commands_with, CliApp},
5 chainspec::EthereumChainSpecParser,
6};
7use clap::{Parser, Subcommand};
8use reth_chainspec::{ChainSpec, EthChainSpec, Hardforks};
9use reth_cli::chainspec::ChainSpecParser;
10use reth_cli_commands::{
11 common::{CliComponentsBuilder, CliNodeTypes, HeaderMut},
12 config_cmd, db, download, dump_genesis, export_era, import, import_era, init_cmd, init_state,
13 launcher::FnLauncher,
14 node::{self, NoArgs},
15 p2p, prune, re_execute, stage,
16};
17use reth_cli_runner::CliRunner;
18use reth_db::DatabaseEnv;
19use reth_node_api::NodePrimitives;
20use reth_node_builder::{NodeBuilder, WithLaunchContext};
21use reth_node_core::{
22 args::{LogArgs, OtlpInitStatus, TraceArgs},
23 version::version_metadata,
24};
25use reth_node_metrics::recorder::install_prometheus_recorder;
26use reth_rpc_server_types::{DefaultRpcModuleValidator, RpcModuleValidator};
27use reth_tracing::{FileWorkerGuard, Layers};
28use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
29use tracing::{info, warn};
30
31#[derive(Debug, Parser)]
35#[command(author, name = version_metadata().name_client.as_ref(), version = version_metadata().short_version.as_ref(), long_version = version_metadata().long_version.as_ref(), about = "Reth", long_about = None)]
36pub struct Cli<
37 C: ChainSpecParser = EthereumChainSpecParser,
38 Ext: clap::Args + fmt::Debug = NoArgs,
39 Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
40> {
41 #[command(subcommand)]
43 pub command: Commands<C, Ext>,
44
45 #[command(flatten)]
47 pub logs: LogArgs,
48
49 #[command(flatten)]
51 pub traces: TraceArgs,
52
53 #[arg(skip)]
55 pub _phantom: PhantomData<Rpc>,
56}
57
58impl Cli {
59 pub fn parse_args() -> Self {
61 Self::parse()
62 }
63
64 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
66 where
67 I: IntoIterator<Item = T>,
68 T: Into<OsString> + Clone,
69 {
70 Self::try_parse_from(itr)
71 }
72}
73
74impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> Cli<C, Ext, Rpc> {
75 pub fn configure(self) -> CliApp<C, Ext, Rpc> {
80 CliApp::new(self)
81 }
82
83 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
126 where
127 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
128 Fut: Future<Output = eyre::Result<()>>,
129 C: ChainSpecParser<ChainSpec = ChainSpec>,
130 {
131 self.with_runner(CliRunner::try_default_runtime()?, launcher)
132 }
133
134 pub fn run_with_components<N>(
141 self,
142 components: impl CliComponentsBuilder<N>,
143 launcher: impl AsyncFnOnce(
144 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
145 Ext,
146 ) -> eyre::Result<()>,
147 ) -> eyre::Result<()>
148 where
149 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
150 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
151 {
152 self.with_runner_and_components(CliRunner::try_default_runtime()?, components, launcher)
153 }
154
155 pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
175 where
176 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
177 Fut: Future<Output = eyre::Result<()>>,
178 C: ChainSpecParser<ChainSpec = ChainSpec>,
179 {
180 let mut app = self.configure();
181 app.set_runner(runner);
182 app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
183 }
184
185 pub fn with_runner_and_components<N>(
188 mut self,
189 runner: CliRunner,
190 components: impl CliComponentsBuilder<N>,
191 launcher: impl AsyncFnOnce(
192 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
193 Ext,
194 ) -> eyre::Result<()>,
195 ) -> eyre::Result<()>
196 where
197 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
198 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
199 {
200 if let Some(chain_spec) = self.command.chain_spec() {
202 self.logs.log_file_directory =
203 self.logs.log_file_directory.join(chain_spec.chain().to_string());
204 }
205 let _guard = self.init_tracing(&runner, Layers::new())?;
206
207 let _ = install_prometheus_recorder();
209
210 run_commands_with::<C, Ext, Rpc, N>(self, runner, components, launcher)
212 }
213
214 pub fn init_tracing(
221 &mut self,
222 runner: &CliRunner,
223 mut layers: Layers,
224 ) -> eyre::Result<Option<FileWorkerGuard>> {
225 let otlp_status = runner.block_on(self.traces.init_otlp_tracing(&mut layers))?;
226
227 let guard = self.logs.init_tracing_with_layers(layers)?;
228 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
229 match otlp_status {
230 OtlpInitStatus::Started(endpoint) => {
231 info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.traces.protocol);
232 }
233 OtlpInitStatus::NoFeature => {
234 warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature")
235 }
236 OtlpInitStatus::Disabled => {}
237 }
238
239 Ok(guard)
240 }
241}
242
243#[derive(Debug, Subcommand)]
245pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
246 #[command(name = "node")]
248 Node(Box<node::NodeCommand<C, Ext>>),
249 #[command(name = "init")]
251 Init(init_cmd::InitCommand<C>),
252 #[command(name = "init-state")]
254 InitState(init_state::InitStateCommand<C>),
255 #[command(name = "import")]
257 Import(import::ImportCommand<C>),
258 #[command(name = "import-era")]
260 ImportEra(import_era::ImportEraCommand<C>),
261 #[command(name = "export-era")]
263 ExportEra(export_era::ExportEraCommand<C>),
264 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
266 #[command(name = "db")]
268 Db(Box<db::Command<C>>),
269 #[command(name = "download")]
271 Download(download::DownloadCommand<C>),
272 #[command(name = "stage")]
274 Stage(stage::Command<C>),
275 #[command(name = "p2p")]
277 P2P(Box<p2p::Command<C>>),
278 #[cfg(feature = "dev")]
280 #[command(name = "test-vectors")]
281 TestVectors(reth_cli_commands::test_vectors::Command),
282 #[command(name = "config")]
284 Config(config_cmd::Command),
285 #[command(name = "prune")]
287 Prune(prune::PruneCommand<C>),
288 #[command(name = "re-execute")]
290 ReExecute(re_execute::Command<C>),
291}
292
293impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
294 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
296 match self {
297 Self::Node(cmd) => cmd.chain_spec(),
298 Self::Init(cmd) => cmd.chain_spec(),
299 Self::InitState(cmd) => cmd.chain_spec(),
300 Self::Import(cmd) => cmd.chain_spec(),
301 Self::ExportEra(cmd) => cmd.chain_spec(),
302 Self::ImportEra(cmd) => cmd.chain_spec(),
303 Self::DumpGenesis(cmd) => cmd.chain_spec(),
304 Self::Db(cmd) => cmd.chain_spec(),
305 Self::Download(cmd) => cmd.chain_spec(),
306 Self::Stage(cmd) => cmd.chain_spec(),
307 Self::P2P(cmd) => cmd.chain_spec(),
308 #[cfg(feature = "dev")]
309 Self::TestVectors(_) => None,
310 Self::Config(_) => None,
311 Self::Prune(cmd) => cmd.chain_spec(),
312 Self::ReExecute(cmd) => cmd.chain_spec(),
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::chainspec::SUPPORTED_CHAINS;
321 use clap::CommandFactory;
322 use reth_chainspec::SEPOLIA;
323 use reth_node_core::args::ColorMode;
324
325 #[test]
326 fn parse_color_mode() {
327 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
328 assert_eq!(reth.logs.color, ColorMode::Always);
329 }
330
331 #[test]
335 fn test_parse_help_all_subcommands() {
336 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
337 for sub_command in reth.get_subcommands() {
338 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
339 .err()
340 .unwrap_or_else(|| {
341 panic!("Failed to parse help message {}", sub_command.get_name())
342 });
343
344 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
347 }
348 }
349
350 #[test]
353 fn parse_logs_path_node() {
354 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
355 if let Some(chain_spec) = reth.command.chain_spec() {
356 reth.logs.log_file_directory =
357 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
358 }
359 let log_dir = reth.logs.log_file_directory;
360 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
361 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
362
363 let mut iter = SUPPORTED_CHAINS.iter();
364 iter.next();
365 for chain in iter {
366 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
367 let chain =
368 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
369 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
370 let log_dir = reth.logs.log_file_directory;
371 let end = format!("reth/logs/{chain}");
372 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
373 }
374 }
375
376 #[test]
379 fn parse_logs_path_init() {
380 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
381 if let Some(chain_spec) = reth.command.chain_spec() {
382 reth.logs.log_file_directory =
383 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
384 }
385 let log_dir = reth.logs.log_file_directory;
386 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
387 println!("{log_dir:?}");
388 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
389 }
390
391 #[test]
393 fn parse_empty_logs_path() {
394 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
395 if let Some(chain_spec) = reth.command.chain_spec() {
396 reth.logs.log_file_directory =
397 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
398 }
399 let log_dir = reth.logs.log_file_directory;
400 let end = "reth/logs".to_string();
401 println!("{log_dir:?}");
402 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
403 }
404
405 #[test]
406 fn parse_env_filter_directives() {
407 let temp_dir = tempfile::tempdir().unwrap();
408
409 unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
410 let reth = Cli::try_parse_args_from([
411 "reth",
412 "init",
413 "--datadir",
414 temp_dir.path().to_str().unwrap(),
415 "--log.file.filter",
416 "debug,net=trace",
417 ])
418 .unwrap();
419 assert!(reth.run(async move |_, _| Ok(())).is_ok());
420 }
421
422 #[test]
423 fn test_rpc_module_validation() {
424 use reth_rpc_server_types::RethRpcModule;
425
426 let cli =
428 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
429
430 if let Commands::Node(command) = &cli.command {
431 if let Some(http_api) = &command.rpc.http_api {
432 let modules = http_api.to_selection();
434 assert!(modules.contains(&RethRpcModule::Eth));
435 assert!(modules.contains(&RethRpcModule::Admin));
436 assert!(modules.contains(&RethRpcModule::Debug));
437 } else {
438 panic!("Expected http.api to be set");
439 }
440 } else {
441 panic!("Expected Node command");
442 }
443
444 let cli =
446 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
447
448 if let Commands::Node(command) = &cli.command {
449 if let Some(http_api) = &command.rpc.http_api {
450 let modules = http_api.to_selection();
451 assert!(modules.contains(&RethRpcModule::Eth));
452 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
453 } else {
454 panic!("Expected http.api to be set");
455 }
456 } else {
457 panic!("Expected Node command");
458 }
459 }
460
461 #[test]
462 fn test_rpc_module_unknown_rejected() {
463 use reth_cli_runner::CliRunner;
464
465 let cli =
467 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
468
469 let runner = CliRunner::try_default_runtime().unwrap();
471 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
472
473 assert!(result.is_err());
474 let err = result.unwrap_err();
475 let err_msg = err.to_string();
476
477 assert!(
479 err_msg.contains("Unknown RPC module"),
480 "Error should mention unknown module: {}",
481 err_msg
482 );
483 assert!(
484 err_msg.contains("'unknownmodule'"),
485 "Error should mention the module name: {}",
486 err_msg
487 );
488 }
489
490 #[test]
491 fn parse_unwind_chain() {
492 let cli = Cli::try_parse_args_from([
493 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
494 ])
495 .unwrap();
496 match cli.command {
497 Commands::Stage(cmd) => match cmd.command {
498 stage::Subcommands::Unwind(cmd) => {
499 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
500 }
501 _ => panic!("Expected Unwind command"),
502 },
503 _ => panic!("Expected Stage command"),
504 };
505 }
506
507 #[test]
508 fn parse_empty_supported_chains() {
509 #[derive(Debug, Clone, Default)]
510 struct FileChainSpecParser;
511
512 impl ChainSpecParser for FileChainSpecParser {
513 type ChainSpec = ChainSpec;
514
515 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
516
517 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
518 EthereumChainSpecParser::parse(s)
519 }
520 }
521
522 let cli = Cli::<FileChainSpecParser>::try_parse_from([
523 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
524 ])
525 .unwrap();
526 match cli.command {
527 Commands::Stage(cmd) => match cmd.command {
528 stage::Subcommands::Unwind(cmd) => {
529 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
530 }
531 _ => panic!("Expected Unwind command"),
532 },
533 _ => panic!("Expected Stage command"),
534 };
535 }
536}