1use crate::{app::CliApp, chainspec::EthereumChainSpecParser};
4use clap::{Parser, Subcommand};
5use reth_chainspec::{ChainSpec, Hardforks};
6use reth_cli::chainspec::ChainSpecParser;
7use reth_cli_commands::{
8 common::{CliComponentsBuilder, CliNodeTypes, HeaderMut},
9 config_cmd, db, download,
10 download::manifest_cmd,
11 dump_genesis, export_era, import, import_era, init_cmd, init_state,
12 launcher::FnLauncher,
13 node::{self, NoArgs},
14 p2p, prune, re_execute, stage,
15};
16use reth_cli_runner::CliRunner;
17use reth_db::DatabaseEnv;
18use reth_node_api::NodePrimitives;
19use reth_node_builder::{NodeBuilder, WithLaunchContext};
20use reth_node_core::{
21 args::{LogArgs, OtlpInitStatus, OtlpLogsStatus, TraceArgs},
22 version::version_metadata,
23};
24use reth_rpc_server_types::{DefaultRpcModuleValidator, RethRpcModule, RpcModuleValidator};
25use reth_tracing::{Layers, TracingGuards};
26use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
27use tracing::{info, warn};
28
29#[derive(Debug, Parser)]
33#[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)]
34pub struct Cli<
35 C: ChainSpecParser = EthereumChainSpecParser,
36 Ext: clap::Args + fmt::Debug = NoArgs,
37 Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
38 SubCmd: Subcommand + fmt::Debug = NoSubCmd,
39> {
40 #[command(subcommand)]
42 pub command: Commands<C, Ext, SubCmd>,
43
44 #[command(flatten)]
46 pub logs: LogArgs,
47
48 #[command(flatten)]
50 pub traces: TraceArgs,
51
52 #[arg(skip)]
54 pub _phantom: PhantomData<Rpc>,
55}
56
57impl Cli {
58 pub fn parse_args() -> Self {
60 Self::parse()
61 }
62
63 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
65 where
66 I: IntoIterator<Item = T>,
67 T: Into<OsString> + Clone,
68 {
69 Self::try_parse_from(itr)
70 }
71}
72
73impl<C, Ext, Rpc, SubCmd> Cli<C, Ext, Rpc, SubCmd>
74where
75 C: ChainSpecParser,
76 Ext: clap::Args + fmt::Debug,
77 Rpc: RpcModuleValidator,
78 SubCmd: Subcommand + fmt::Debug,
79{
80 pub fn as_node_command_mut(&mut self) -> Option<&mut node::NodeCommand<C, Ext>> {
82 match &mut self.command {
83 Commands::Node(command) => Some(command.as_mut()),
84 _ => None,
85 }
86 }
87
88 pub fn apply_node_command(
90 &mut self,
91 f: impl FnOnce(&mut node::NodeCommand<C, Ext>),
92 ) -> &mut Self {
93 if let Some(command) = self.as_node_command_mut() {
94 f(command);
95 }
96
97 self
98 }
99}
100
101impl<
102 C: ChainSpecParser,
103 Ext: clap::Args + fmt::Debug,
104 Rpc: RpcModuleValidator,
105 SubCmd: crate::app::ExtendedCommand + Subcommand + fmt::Debug,
106 > Cli<C, Ext, Rpc, SubCmd>
107{
108 pub fn configure(self) -> CliApp<C, Ext, Rpc, SubCmd> {
113 CliApp::new(self)
114 }
115
116 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
159 where
160 L: FnOnce(WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>, Ext) -> Fut,
161 Fut: Future<Output = eyre::Result<()>>,
162 C: ChainSpecParser<ChainSpec = ChainSpec>,
163 {
164 self.configure()
165 .run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
166 }
167
168 pub fn run_with_components<N>(
175 self,
176 components: impl CliComponentsBuilder<N>,
177 launcher: impl AsyncFnOnce(
178 WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>,
179 Ext,
180 ) -> eyre::Result<()>,
181 ) -> eyre::Result<()>
182 where
183 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
184 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
185 {
186 self.configure().run_with_components(components, launcher)
187 }
188
189 pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
209 where
210 L: FnOnce(WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>, Ext) -> Fut,
211 Fut: Future<Output = eyre::Result<()>>,
212 C: ChainSpecParser<ChainSpec = ChainSpec>,
213 {
214 let mut app = self.configure();
215 app.set_runner(runner);
216 app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
217 }
218
219 pub fn with_runner_and_components<N>(
222 self,
223 runner: CliRunner,
224 components: impl CliComponentsBuilder<N>,
225 launcher: impl AsyncFnOnce(
226 WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>,
227 Ext,
228 ) -> eyre::Result<()>,
229 ) -> eyre::Result<()>
230 where
231 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
232 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
233 {
234 let mut app = self.configure();
235 app.set_runner(runner);
236 app.run_with_components(components, launcher)
237 }
238
239 pub fn init_tracing(
246 &mut self,
247 runner: &CliRunner,
248 mut layers: Layers,
249 ) -> eyre::Result<TracingGuards> {
250 let otlp_status = runner.block_on(self.traces.init_otlp_tracing(&mut layers))?;
251 let otlp_logs_status = runner.block_on(self.traces.init_otlp_logs(&mut layers))?;
252
253 let enable_reload = self.command.debug_namespace_enabled();
255 let guards = self.logs.init_tracing_with_layers(layers, enable_reload)?;
256 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
257
258 match otlp_status {
259 OtlpInitStatus::Started(endpoint) => {
260 info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.traces.protocol);
261 }
262 OtlpInitStatus::NoFeature => {
263 warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature")
264 }
265 OtlpInitStatus::Disabled => {}
266 }
267
268 match otlp_logs_status {
269 OtlpLogsStatus::Started(endpoint) => {
270 info!(target: "reth::cli", "Started OTLP {:?} logs export to {endpoint}", self.traces.protocol);
271 }
272 OtlpLogsStatus::NoFeature => {
273 warn!(target: "reth::cli", "Provided OTLP logs arguments do not have effect, compile with the `otlp-logs` feature")
274 }
275 OtlpLogsStatus::Disabled => {}
276 }
277
278 Ok(guards)
279 }
280}
281
282#[derive(Debug, Subcommand)]
284pub enum Commands<
285 C: ChainSpecParser,
286 Ext: clap::Args + fmt::Debug,
287 SubCmd: Subcommand + fmt::Debug = NoSubCmd,
288> {
289 #[command(name = "node")]
291 Node(Box<node::NodeCommand<C, Ext>>),
292 #[command(name = "init")]
294 Init(init_cmd::InitCommand<C>),
295 #[command(name = "init-state")]
297 InitState(init_state::InitStateCommand<C>),
298 #[command(name = "import")]
300 Import(import::ImportCommand<C>),
301 #[command(name = "import-era")]
303 ImportEra(import_era::ImportEraCommand<C>),
304 #[command(name = "export-era")]
306 ExportEra(export_era::ExportEraCommand<C>),
307 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
309 #[command(name = "db")]
311 Db(Box<db::Command<C>>),
312 #[command(name = "download")]
314 Download(download::DownloadCommand<C>),
315 #[command(name = "snapshot-manifest")]
317 SnapshotManifest(manifest_cmd::SnapshotManifestCommand),
318 #[command(name = "stage")]
320 Stage(stage::Command<C>),
321 #[command(name = "p2p")]
323 P2P(Box<p2p::Command<C>>),
324 #[cfg(feature = "dev")]
326 #[command(name = "test-vectors")]
327 TestVectors(reth_cli_commands::test_vectors::Command),
328 #[command(name = "config")]
330 Config(config_cmd::Command),
331 #[command(name = "prune")]
333 Prune(prune::PruneCommand<C>),
334 #[command(name = "re-execute")]
336 ReExecute(re_execute::Command<C>),
337 #[command(flatten)]
339 Ext(SubCmd),
340}
341
342#[derive(Debug, Subcommand)]
347pub enum NoSubCmd {}
348
349impl crate::app::ExtendedCommand for NoSubCmd {
350 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
351 match self {}
352 }
353}
354
355impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, SubCmd: Subcommand + fmt::Debug>
356 Commands<C, Ext, SubCmd>
357{
358 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
360 match self {
361 Self::Node(cmd) => cmd.chain_spec(),
362 Self::Init(cmd) => cmd.chain_spec(),
363 Self::InitState(cmd) => cmd.chain_spec(),
364 Self::Import(cmd) => cmd.chain_spec(),
365 Self::ExportEra(cmd) => cmd.chain_spec(),
366 Self::ImportEra(cmd) => cmd.chain_spec(),
367 Self::DumpGenesis(cmd) => cmd.chain_spec(),
368 Self::Db(cmd) => cmd.chain_spec(),
369 Self::Download(cmd) => cmd.chain_spec(),
370 Self::SnapshotManifest(_) => None,
371 Self::Stage(cmd) => cmd.chain_spec(),
372 Self::P2P(cmd) => cmd.chain_spec(),
373 #[cfg(feature = "dev")]
374 Self::TestVectors(_) => None,
375 Self::Config(_) => None,
376 Self::Prune(cmd) => cmd.chain_spec(),
377 Self::ReExecute(cmd) => cmd.chain_spec(),
378 Self::Ext(_) => None,
379 }
380 }
381
382 pub fn debug_namespace_enabled(&self) -> bool {
386 match self {
387 Self::Node(cmd) => cmd.rpc.is_namespace_enabled(RethRpcModule::Debug),
388 _ => false,
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::{app::ExtendedCommand, chainspec::SUPPORTED_CHAINS};
397 use clap::CommandFactory;
398 use reth_chainspec::{EthChainSpec, SEPOLIA};
399 use reth_node_core::args::ColorMode;
400 use std::sync::atomic::{AtomicBool, Ordering};
401
402 #[test]
403 fn parse_color_mode() {
404 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
405 assert_eq!(reth.logs.color, ColorMode::Always);
406 }
407
408 #[test]
409 fn node_command_mut_accessor_returns_node_command() {
410 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
411
412 let node_command = reth.as_node_command_mut().expect("expected node command");
413 node_command.with_unused_ports = true;
414
415 assert!(reth.as_node_command_mut().unwrap().with_unused_ports);
416 }
417
418 #[test]
419 fn apply_node_command_only_runs_for_node_command() {
420 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
421 reth.apply_node_command(|node_command| node_command.with_unused_ports = true);
422 assert!(reth.as_node_command_mut().unwrap().with_unused_ports);
423
424 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
425 let mut applied = false;
426 reth.apply_node_command(|_| applied = true);
427
428 assert!(reth.as_node_command_mut().is_none());
429 assert!(!applied);
430 }
431
432 #[test]
436 fn test_parse_help_all_subcommands() {
437 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
438 for sub_command in reth.get_subcommands() {
439 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
440 .err()
441 .unwrap_or_else(|| {
442 panic!("Failed to parse help message {}", sub_command.get_name())
443 });
444
445 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
448 }
449 }
450
451 #[test]
454 fn parse_logs_path_node() {
455 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
456 if let Some(chain_spec) = reth.command.chain_spec() {
457 reth.logs.log_file_directory =
458 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
459 }
460 let log_dir = reth.logs.log_file_directory;
461 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
462 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
463
464 let mut iter = SUPPORTED_CHAINS.iter();
465 iter.next();
466 for chain in iter {
467 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
468 let chain =
469 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
470 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
471 let log_dir = reth.logs.log_file_directory;
472 let end = format!("reth/logs/{chain}");
473 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
474 }
475 }
476
477 #[test]
480 fn parse_logs_path_init() {
481 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
482 if let Some(chain_spec) = reth.command.chain_spec() {
483 reth.logs.log_file_directory =
484 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
485 }
486 let log_dir = reth.logs.log_file_directory;
487 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
488 println!("{log_dir:?}");
489 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
490 }
491
492 #[test]
494 fn parse_empty_logs_path() {
495 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
496 if let Some(chain_spec) = reth.command.chain_spec() {
497 reth.logs.log_file_directory =
498 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
499 }
500 let log_dir = reth.logs.log_file_directory;
501 let end = "reth/logs".to_string();
502 println!("{log_dir:?}");
503 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
504 }
505
506 #[test]
507 fn log_file_max_files_defaults() {
508 use reth_node_core::args::LogArgs;
509
510 let mut cli = Cli::try_parse_args_from(["reth", "node"]).unwrap();
513 assert!(cli.logs.log_file_max_files.is_none());
514 cli.logs.apply_node_defaults();
515 assert_eq!(cli.logs.log_file_max_files, Some(LogArgs::DEFAULT_MAX_LOG_FILES_NODE));
516
517 let cli = Cli::try_parse_args_from(["reth", "config"]).unwrap();
520 assert!(cli.logs.log_file_max_files.is_none());
521 assert_eq!(cli.logs.effective_log_file_max_files(), 0);
522
523 let mut cli =
525 Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "10"]).unwrap();
526 assert_eq!(cli.logs.log_file_max_files, Some(10));
527 cli.logs.apply_node_defaults();
528 assert_eq!(cli.logs.log_file_max_files, Some(10));
529
530 let cli =
532 Cli::try_parse_args_from(["reth", "config", "--log.file.max-files", "3"]).unwrap();
533 assert_eq!(cli.logs.log_file_max_files, Some(3));
534 assert_eq!(cli.logs.effective_log_file_max_files(), 3);
535
536 let cli = Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "0"]).unwrap();
538 assert_eq!(cli.logs.log_file_max_files, Some(0));
539 assert_eq!(cli.logs.effective_log_file_max_files(), 0);
540 }
541
542 #[test]
543 fn parse_env_filter_directives() {
544 let temp_dir = tempfile::tempdir().unwrap();
545
546 let reth = Cli::try_parse_args_from([
547 "reth",
548 "init",
549 "--datadir",
550 temp_dir.path().to_str().unwrap(),
551 "--log.file.filter",
552 "debug,net=trace",
553 ])
554 .unwrap();
555 assert!(reth.run(async move |_, _| Ok(())).is_ok());
556 }
557
558 #[test]
559 fn test_rpc_module_validation() {
560 use reth_rpc_server_types::RethRpcModule;
561
562 let cli =
564 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
565
566 if let Commands::Node(command) = &cli.command {
567 if let Some(http_api) = &command.rpc.http_api {
568 let modules = http_api.to_selection();
570 assert!(modules.contains(&RethRpcModule::Eth));
571 assert!(modules.contains(&RethRpcModule::Admin));
572 assert!(modules.contains(&RethRpcModule::Debug));
573 } else {
574 panic!("Expected http.api to be set");
575 }
576 } else {
577 panic!("Expected Node command");
578 }
579
580 let cli =
582 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
583
584 if let Commands::Node(command) = &cli.command {
585 if let Some(http_api) = &command.rpc.http_api {
586 let modules = http_api.to_selection();
587 assert!(modules.contains(&RethRpcModule::Eth));
588 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
589 } else {
590 panic!("Expected http.api to be set");
591 }
592 } else {
593 panic!("Expected Node command");
594 }
595 }
596
597 #[test]
598 fn test_rpc_module_unknown_rejected() {
599 use reth_cli_runner::CliRunner;
600
601 let cli =
603 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
604
605 let runner = CliRunner::try_default_runtime().unwrap();
607 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
608
609 assert!(result.is_err());
610 let err = result.unwrap_err();
611 let err_msg = err.to_string();
612
613 assert!(
615 err_msg.contains("Unknown RPC module"),
616 "Error should mention unknown module: {}",
617 err_msg
618 );
619 assert!(
620 err_msg.contains("'unknownmodule'"),
621 "Error should mention the module name: {}",
622 err_msg
623 );
624 }
625
626 #[test]
627 fn parse_unwind_chain() {
628 let cli = Cli::try_parse_args_from([
629 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
630 ])
631 .unwrap();
632 match cli.command {
633 Commands::Stage(cmd) => match cmd.command {
634 stage::Subcommands::Unwind(cmd) => {
635 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
636 }
637 _ => panic!("Expected Unwind command"),
638 },
639 _ => panic!("Expected Stage command"),
640 };
641 }
642
643 #[test]
644 fn parse_empty_supported_chains() {
645 #[derive(Debug, Clone, Default)]
646 struct FileChainSpecParser;
647
648 impl ChainSpecParser for FileChainSpecParser {
649 type ChainSpec = ChainSpec;
650
651 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
652
653 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
654 EthereumChainSpecParser::parse(s)
655 }
656 }
657
658 let cli = Cli::<FileChainSpecParser>::try_parse_from([
659 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
660 ])
661 .unwrap();
662 match cli.command {
663 Commands::Stage(cmd) => match cmd.command {
664 stage::Subcommands::Unwind(cmd) => {
665 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
666 }
667 _ => panic!("Expected Unwind command"),
668 },
669 _ => panic!("Expected Stage command"),
670 };
671 }
672
673 #[test]
674 fn test_extensible_subcommands() {
675 use reth_cli_runner::CliRunner;
676 use reth_rpc_server_types::DefaultRpcModuleValidator;
677
678 #[derive(Debug, Subcommand)]
679 enum CustomCommands {
680 #[command(name = "hello")]
682 Hello {
683 #[arg(long)]
685 name: String,
686 },
687 #[command(name = "goodbye")]
689 Goodbye,
690 }
691
692 static EXECUTED: AtomicBool = AtomicBool::new(false);
693
694 impl ExtendedCommand for CustomCommands {
695 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
696 match self {
697 Self::Hello { name } => {
698 assert_eq!(name, "world");
699 EXECUTED.store(true, Ordering::SeqCst);
700 Ok(())
701 }
702 Self::Goodbye => Ok(()),
703 }
704 }
705 }
706
707 let cli = Cli::<
709 EthereumChainSpecParser,
710 NoArgs,
711 DefaultRpcModuleValidator,
712 CustomCommands,
713 >::try_parse_from(["reth", "hello", "--name", "world"])
714 .unwrap();
715
716 match &cli.command {
717 Commands::Ext(CustomCommands::Hello { name }) => {
718 assert_eq!(name, "world");
719 }
720 _ => panic!("Expected Ext(Hello) command"),
721 }
722
723 let cli = Cli::<
725 EthereumChainSpecParser,
726 NoArgs,
727 DefaultRpcModuleValidator,
728 CustomCommands,
729 >::try_parse_from(["reth", "goodbye"])
730 .unwrap();
731
732 match &cli.command {
733 Commands::Ext(CustomCommands::Goodbye) => {}
734 _ => panic!("Expected Ext(Goodbye) command"),
735 }
736
737 let cli = Cli::<
739 EthereumChainSpecParser,
740 NoArgs,
741 DefaultRpcModuleValidator,
742 CustomCommands,
743 >::try_parse_from(["reth", "node"])
744 .unwrap();
745
746 match &cli.command {
747 Commands::Node(_) => {}
748 _ => panic!("Expected Node command"),
749 }
750
751 let cli = Cli::<
753 EthereumChainSpecParser,
754 NoArgs,
755 DefaultRpcModuleValidator,
756 CustomCommands,
757 >::try_parse_from(["reth", "hello", "--name", "world"])
758 .unwrap();
759
760 if let Commands::Ext(cmd) = cli.command {
761 let runner = CliRunner::try_default_runtime().unwrap();
762 cmd.execute(runner).unwrap();
763 assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
764 }
765 }
766}