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::{FileWorkerGuard, Layers};
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<
74 C: ChainSpecParser,
75 Ext: clap::Args + fmt::Debug,
76 Rpc: RpcModuleValidator,
77 SubCmd: crate::app::ExtendedCommand + Subcommand + fmt::Debug,
78 > Cli<C, Ext, Rpc, SubCmd>
79{
80 pub fn configure(self) -> CliApp<C, Ext, Rpc, SubCmd> {
85 CliApp::new(self)
86 }
87
88 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
131 where
132 L: FnOnce(WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>, Ext) -> Fut,
133 Fut: Future<Output = eyre::Result<()>>,
134 C: ChainSpecParser<ChainSpec = ChainSpec>,
135 {
136 self.configure()
137 .run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
138 }
139
140 pub fn run_with_components<N>(
147 self,
148 components: impl CliComponentsBuilder<N>,
149 launcher: impl AsyncFnOnce(
150 WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>,
151 Ext,
152 ) -> eyre::Result<()>,
153 ) -> eyre::Result<()>
154 where
155 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
156 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
157 {
158 self.configure().run_with_components(components, launcher)
159 }
160
161 pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
181 where
182 L: FnOnce(WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>, Ext) -> Fut,
183 Fut: Future<Output = eyre::Result<()>>,
184 C: ChainSpecParser<ChainSpec = ChainSpec>,
185 {
186 let mut app = self.configure();
187 app.set_runner(runner);
188 app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
189 }
190
191 pub fn with_runner_and_components<N>(
194 self,
195 runner: CliRunner,
196 components: impl CliComponentsBuilder<N>,
197 launcher: impl AsyncFnOnce(
198 WithLaunchContext<NodeBuilder<DatabaseEnv, C::ChainSpec>>,
199 Ext,
200 ) -> eyre::Result<()>,
201 ) -> eyre::Result<()>
202 where
203 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
204 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
205 {
206 let mut app = self.configure();
207 app.set_runner(runner);
208 app.run_with_components(components, launcher)
209 }
210
211 pub fn init_tracing(
219 &mut self,
220 runner: &CliRunner,
221 mut layers: Layers,
222 ) -> eyre::Result<Option<FileWorkerGuard>> {
223 let otlp_status = runner.block_on(self.traces.init_otlp_tracing(&mut layers))?;
224 let otlp_logs_status = runner.block_on(self.traces.init_otlp_logs(&mut layers))?;
225
226 let enable_reload = self.command.debug_namespace_enabled();
228 let file_guard = self.logs.init_tracing_with_layers(layers, enable_reload)?;
229 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
230
231 match otlp_status {
232 OtlpInitStatus::Started(endpoint) => {
233 info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.traces.protocol);
234 }
235 OtlpInitStatus::NoFeature => {
236 warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature")
237 }
238 OtlpInitStatus::Disabled => {}
239 }
240
241 match otlp_logs_status {
242 OtlpLogsStatus::Started(endpoint) => {
243 info!(target: "reth::cli", "Started OTLP {:?} logs export to {endpoint}", self.traces.protocol);
244 }
245 OtlpLogsStatus::NoFeature => {
246 warn!(target: "reth::cli", "Provided OTLP logs arguments do not have effect, compile with the `otlp-logs` feature")
247 }
248 OtlpLogsStatus::Disabled => {}
249 }
250
251 Ok(file_guard)
252 }
253}
254
255#[derive(Debug, Subcommand)]
257pub enum Commands<
258 C: ChainSpecParser,
259 Ext: clap::Args + fmt::Debug,
260 SubCmd: Subcommand + fmt::Debug = NoSubCmd,
261> {
262 #[command(name = "node")]
264 Node(Box<node::NodeCommand<C, Ext>>),
265 #[command(name = "init")]
267 Init(init_cmd::InitCommand<C>),
268 #[command(name = "init-state")]
270 InitState(init_state::InitStateCommand<C>),
271 #[command(name = "import")]
273 Import(import::ImportCommand<C>),
274 #[command(name = "import-era")]
276 ImportEra(import_era::ImportEraCommand<C>),
277 #[command(name = "export-era")]
279 ExportEra(export_era::ExportEraCommand<C>),
280 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
282 #[command(name = "db")]
284 Db(Box<db::Command<C>>),
285 #[command(name = "download")]
287 Download(download::DownloadCommand<C>),
288 #[command(name = "snapshot-manifest")]
290 SnapshotManifest(manifest_cmd::SnapshotManifestCommand),
291 #[command(name = "stage")]
293 Stage(stage::Command<C>),
294 #[command(name = "p2p")]
296 P2P(Box<p2p::Command<C>>),
297 #[cfg(feature = "dev")]
299 #[command(name = "test-vectors")]
300 TestVectors(reth_cli_commands::test_vectors::Command),
301 #[command(name = "config")]
303 Config(config_cmd::Command),
304 #[command(name = "prune")]
306 Prune(prune::PruneCommand<C>),
307 #[command(name = "re-execute")]
309 ReExecute(re_execute::Command<C>),
310 #[command(flatten)]
312 Ext(SubCmd),
313}
314
315#[derive(Debug, Subcommand)]
320pub enum NoSubCmd {}
321
322impl crate::app::ExtendedCommand for NoSubCmd {
323 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
324 match self {}
325 }
326}
327
328impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, SubCmd: Subcommand + fmt::Debug>
329 Commands<C, Ext, SubCmd>
330{
331 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
333 match self {
334 Self::Node(cmd) => cmd.chain_spec(),
335 Self::Init(cmd) => cmd.chain_spec(),
336 Self::InitState(cmd) => cmd.chain_spec(),
337 Self::Import(cmd) => cmd.chain_spec(),
338 Self::ExportEra(cmd) => cmd.chain_spec(),
339 Self::ImportEra(cmd) => cmd.chain_spec(),
340 Self::DumpGenesis(cmd) => cmd.chain_spec(),
341 Self::Db(cmd) => cmd.chain_spec(),
342 Self::Download(cmd) => cmd.chain_spec(),
343 Self::SnapshotManifest(_) => None,
344 Self::Stage(cmd) => cmd.chain_spec(),
345 Self::P2P(cmd) => cmd.chain_spec(),
346 #[cfg(feature = "dev")]
347 Self::TestVectors(_) => None,
348 Self::Config(_) => None,
349 Self::Prune(cmd) => cmd.chain_spec(),
350 Self::ReExecute(cmd) => cmd.chain_spec(),
351 Self::Ext(_) => None,
352 }
353 }
354
355 pub fn debug_namespace_enabled(&self) -> bool {
359 match self {
360 Self::Node(cmd) => cmd.rpc.is_namespace_enabled(RethRpcModule::Debug),
361 _ => false,
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::chainspec::SUPPORTED_CHAINS;
370 use clap::CommandFactory;
371 use reth_chainspec::{EthChainSpec, SEPOLIA};
372 use reth_node_core::args::ColorMode;
373
374 #[test]
375 fn parse_color_mode() {
376 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
377 assert_eq!(reth.logs.color, ColorMode::Always);
378 }
379
380 #[test]
384 fn test_parse_help_all_subcommands() {
385 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
386 for sub_command in reth.get_subcommands() {
387 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
388 .err()
389 .unwrap_or_else(|| {
390 panic!("Failed to parse help message {}", sub_command.get_name())
391 });
392
393 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
396 }
397 }
398
399 #[test]
402 fn parse_logs_path_node() {
403 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
404 if let Some(chain_spec) = reth.command.chain_spec() {
405 reth.logs.log_file_directory =
406 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
407 }
408 let log_dir = reth.logs.log_file_directory;
409 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
410 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
411
412 let mut iter = SUPPORTED_CHAINS.iter();
413 iter.next();
414 for chain in iter {
415 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
416 let chain =
417 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
418 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
419 let log_dir = reth.logs.log_file_directory;
420 let end = format!("reth/logs/{chain}");
421 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
422 }
423 }
424
425 #[test]
428 fn parse_logs_path_init() {
429 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
430 if let Some(chain_spec) = reth.command.chain_spec() {
431 reth.logs.log_file_directory =
432 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
433 }
434 let log_dir = reth.logs.log_file_directory;
435 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
436 println!("{log_dir:?}");
437 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
438 }
439
440 #[test]
442 fn parse_empty_logs_path() {
443 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
444 if let Some(chain_spec) = reth.command.chain_spec() {
445 reth.logs.log_file_directory =
446 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
447 }
448 let log_dir = reth.logs.log_file_directory;
449 let end = "reth/logs".to_string();
450 println!("{log_dir:?}");
451 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
452 }
453
454 #[test]
455 fn log_file_max_files_defaults() {
456 use reth_node_core::args::LogArgs;
457
458 let mut cli = Cli::try_parse_args_from(["reth", "node"]).unwrap();
461 assert!(cli.logs.log_file_max_files.is_none());
462 cli.logs.apply_node_defaults();
463 assert_eq!(cli.logs.log_file_max_files, Some(LogArgs::DEFAULT_MAX_LOG_FILES_NODE));
464
465 let cli = Cli::try_parse_args_from(["reth", "config"]).unwrap();
468 assert!(cli.logs.log_file_max_files.is_none());
469 assert_eq!(cli.logs.effective_log_file_max_files(), 0);
470
471 let mut cli =
473 Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "10"]).unwrap();
474 assert_eq!(cli.logs.log_file_max_files, Some(10));
475 cli.logs.apply_node_defaults();
476 assert_eq!(cli.logs.log_file_max_files, Some(10));
477
478 let cli =
480 Cli::try_parse_args_from(["reth", "config", "--log.file.max-files", "3"]).unwrap();
481 assert_eq!(cli.logs.log_file_max_files, Some(3));
482 assert_eq!(cli.logs.effective_log_file_max_files(), 3);
483
484 let cli = Cli::try_parse_args_from(["reth", "node", "--log.file.max-files", "0"]).unwrap();
486 assert_eq!(cli.logs.log_file_max_files, Some(0));
487 assert_eq!(cli.logs.effective_log_file_max_files(), 0);
488 }
489
490 #[test]
491 fn parse_env_filter_directives() {
492 let temp_dir = tempfile::tempdir().unwrap();
493
494 let reth = Cli::try_parse_args_from([
495 "reth",
496 "init",
497 "--datadir",
498 temp_dir.path().to_str().unwrap(),
499 "--log.file.filter",
500 "debug,net=trace",
501 ])
502 .unwrap();
503 assert!(reth.run(async move |_, _| Ok(())).is_ok());
504 }
505
506 #[test]
507 fn test_rpc_module_validation() {
508 use reth_rpc_server_types::RethRpcModule;
509
510 let cli =
512 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
513
514 if let Commands::Node(command) = &cli.command {
515 if let Some(http_api) = &command.rpc.http_api {
516 let modules = http_api.to_selection();
518 assert!(modules.contains(&RethRpcModule::Eth));
519 assert!(modules.contains(&RethRpcModule::Admin));
520 assert!(modules.contains(&RethRpcModule::Debug));
521 } else {
522 panic!("Expected http.api to be set");
523 }
524 } else {
525 panic!("Expected Node command");
526 }
527
528 let cli =
530 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
531
532 if let Commands::Node(command) = &cli.command {
533 if let Some(http_api) = &command.rpc.http_api {
534 let modules = http_api.to_selection();
535 assert!(modules.contains(&RethRpcModule::Eth));
536 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
537 } else {
538 panic!("Expected http.api to be set");
539 }
540 } else {
541 panic!("Expected Node command");
542 }
543 }
544
545 #[test]
546 fn test_rpc_module_unknown_rejected() {
547 use reth_cli_runner::CliRunner;
548
549 let cli =
551 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
552
553 let runner = CliRunner::try_default_runtime().unwrap();
555 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
556
557 assert!(result.is_err());
558 let err = result.unwrap_err();
559 let err_msg = err.to_string();
560
561 assert!(
563 err_msg.contains("Unknown RPC module"),
564 "Error should mention unknown module: {}",
565 err_msg
566 );
567 assert!(
568 err_msg.contains("'unknownmodule'"),
569 "Error should mention the module name: {}",
570 err_msg
571 );
572 }
573
574 #[test]
575 fn parse_unwind_chain() {
576 let cli = Cli::try_parse_args_from([
577 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
578 ])
579 .unwrap();
580 match cli.command {
581 Commands::Stage(cmd) => match cmd.command {
582 stage::Subcommands::Unwind(cmd) => {
583 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
584 }
585 _ => panic!("Expected Unwind command"),
586 },
587 _ => panic!("Expected Stage command"),
588 };
589 }
590
591 #[test]
592 fn parse_empty_supported_chains() {
593 #[derive(Debug, Clone, Default)]
594 struct FileChainSpecParser;
595
596 impl ChainSpecParser for FileChainSpecParser {
597 type ChainSpec = ChainSpec;
598
599 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
600
601 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
602 EthereumChainSpecParser::parse(s)
603 }
604 }
605
606 let cli = Cli::<FileChainSpecParser>::try_parse_from([
607 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
608 ])
609 .unwrap();
610 match cli.command {
611 Commands::Stage(cmd) => match cmd.command {
612 stage::Subcommands::Unwind(cmd) => {
613 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
614 }
615 _ => panic!("Expected Unwind command"),
616 },
617 _ => panic!("Expected Stage command"),
618 };
619 }
620
621 #[test]
622 fn test_extensible_subcommands() {
623 use crate::app::ExtendedCommand;
624 use reth_cli_runner::CliRunner;
625 use reth_rpc_server_types::DefaultRpcModuleValidator;
626 use std::sync::atomic::{AtomicBool, Ordering};
627
628 #[derive(Debug, Subcommand)]
629 enum CustomCommands {
630 #[command(name = "hello")]
632 Hello {
633 #[arg(long)]
635 name: String,
636 },
637 #[command(name = "goodbye")]
639 Goodbye,
640 }
641
642 static EXECUTED: AtomicBool = AtomicBool::new(false);
643
644 impl ExtendedCommand for CustomCommands {
645 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
646 match self {
647 Self::Hello { name } => {
648 assert_eq!(name, "world");
649 EXECUTED.store(true, Ordering::SeqCst);
650 Ok(())
651 }
652 Self::Goodbye => Ok(()),
653 }
654 }
655 }
656
657 let cli = Cli::<
659 EthereumChainSpecParser,
660 NoArgs,
661 DefaultRpcModuleValidator,
662 CustomCommands,
663 >::try_parse_from(["reth", "hello", "--name", "world"])
664 .unwrap();
665
666 match &cli.command {
667 Commands::Ext(CustomCommands::Hello { name }) => {
668 assert_eq!(name, "world");
669 }
670 _ => panic!("Expected Ext(Hello) command"),
671 }
672
673 let cli = Cli::<
675 EthereumChainSpecParser,
676 NoArgs,
677 DefaultRpcModuleValidator,
678 CustomCommands,
679 >::try_parse_from(["reth", "goodbye"])
680 .unwrap();
681
682 match &cli.command {
683 Commands::Ext(CustomCommands::Goodbye) => {}
684 _ => panic!("Expected Ext(Goodbye) command"),
685 }
686
687 let cli = Cli::<
689 EthereumChainSpecParser,
690 NoArgs,
691 DefaultRpcModuleValidator,
692 CustomCommands,
693 >::try_parse_from(["reth", "node"])
694 .unwrap();
695
696 match &cli.command {
697 Commands::Node(_) => {}
698 _ => panic!("Expected Node command"),
699 }
700
701 let cli = Cli::<
703 EthereumChainSpecParser,
704 NoArgs,
705 DefaultRpcModuleValidator,
706 CustomCommands,
707 >::try_parse_from(["reth", "hello", "--name", "world"])
708 .unwrap();
709
710 if let Commands::Ext(cmd) = cli.command {
711 let runner = CliRunner::try_default_runtime().unwrap();
712 cmd.execute(runner).unwrap();
713 assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
714 }
715 }
716}