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 unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
495 let reth = Cli::try_parse_args_from([
496 "reth",
497 "init",
498 "--datadir",
499 temp_dir.path().to_str().unwrap(),
500 "--log.file.filter",
501 "debug,net=trace",
502 ])
503 .unwrap();
504 assert!(reth.run(async move |_, _| Ok(())).is_ok());
505 }
506
507 #[test]
508 fn test_rpc_module_validation() {
509 use reth_rpc_server_types::RethRpcModule;
510
511 let cli =
513 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
514
515 if let Commands::Node(command) = &cli.command {
516 if let Some(http_api) = &command.rpc.http_api {
517 let modules = http_api.to_selection();
519 assert!(modules.contains(&RethRpcModule::Eth));
520 assert!(modules.contains(&RethRpcModule::Admin));
521 assert!(modules.contains(&RethRpcModule::Debug));
522 } else {
523 panic!("Expected http.api to be set");
524 }
525 } else {
526 panic!("Expected Node command");
527 }
528
529 let cli =
531 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
532
533 if let Commands::Node(command) = &cli.command {
534 if let Some(http_api) = &command.rpc.http_api {
535 let modules = http_api.to_selection();
536 assert!(modules.contains(&RethRpcModule::Eth));
537 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
538 } else {
539 panic!("Expected http.api to be set");
540 }
541 } else {
542 panic!("Expected Node command");
543 }
544 }
545
546 #[test]
547 fn test_rpc_module_unknown_rejected() {
548 use reth_cli_runner::CliRunner;
549
550 let cli =
552 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
553
554 let runner = CliRunner::try_default_runtime().unwrap();
556 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
557
558 assert!(result.is_err());
559 let err = result.unwrap_err();
560 let err_msg = err.to_string();
561
562 assert!(
564 err_msg.contains("Unknown RPC module"),
565 "Error should mention unknown module: {}",
566 err_msg
567 );
568 assert!(
569 err_msg.contains("'unknownmodule'"),
570 "Error should mention the module name: {}",
571 err_msg
572 );
573 }
574
575 #[test]
576 fn parse_unwind_chain() {
577 let cli = Cli::try_parse_args_from([
578 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
579 ])
580 .unwrap();
581 match cli.command {
582 Commands::Stage(cmd) => match cmd.command {
583 stage::Subcommands::Unwind(cmd) => {
584 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
585 }
586 _ => panic!("Expected Unwind command"),
587 },
588 _ => panic!("Expected Stage command"),
589 };
590 }
591
592 #[test]
593 fn parse_empty_supported_chains() {
594 #[derive(Debug, Clone, Default)]
595 struct FileChainSpecParser;
596
597 impl ChainSpecParser for FileChainSpecParser {
598 type ChainSpec = ChainSpec;
599
600 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
601
602 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
603 EthereumChainSpecParser::parse(s)
604 }
605 }
606
607 let cli = Cli::<FileChainSpecParser>::try_parse_from([
608 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
609 ])
610 .unwrap();
611 match cli.command {
612 Commands::Stage(cmd) => match cmd.command {
613 stage::Subcommands::Unwind(cmd) => {
614 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
615 }
616 _ => panic!("Expected Unwind command"),
617 },
618 _ => panic!("Expected Stage command"),
619 };
620 }
621
622 #[test]
623 fn test_extensible_subcommands() {
624 use crate::app::ExtendedCommand;
625 use reth_cli_runner::CliRunner;
626 use reth_rpc_server_types::DefaultRpcModuleValidator;
627 use std::sync::atomic::{AtomicBool, Ordering};
628
629 #[derive(Debug, Subcommand)]
630 enum CustomCommands {
631 #[command(name = "hello")]
633 Hello {
634 #[arg(long)]
636 name: String,
637 },
638 #[command(name = "goodbye")]
640 Goodbye,
641 }
642
643 static EXECUTED: AtomicBool = AtomicBool::new(false);
644
645 impl ExtendedCommand for CustomCommands {
646 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
647 match self {
648 Self::Hello { name } => {
649 assert_eq!(name, "world");
650 EXECUTED.store(true, Ordering::SeqCst);
651 Ok(())
652 }
653 Self::Goodbye => Ok(()),
654 }
655 }
656 }
657
658 let cli = Cli::<
660 EthereumChainSpecParser,
661 NoArgs,
662 DefaultRpcModuleValidator,
663 CustomCommands,
664 >::try_parse_from(["reth", "hello", "--name", "world"])
665 .unwrap();
666
667 match &cli.command {
668 Commands::Ext(CustomCommands::Hello { name }) => {
669 assert_eq!(name, "world");
670 }
671 _ => panic!("Expected Ext(Hello) command"),
672 }
673
674 let cli = Cli::<
676 EthereumChainSpecParser,
677 NoArgs,
678 DefaultRpcModuleValidator,
679 CustomCommands,
680 >::try_parse_from(["reth", "goodbye"])
681 .unwrap();
682
683 match &cli.command {
684 Commands::Ext(CustomCommands::Goodbye) => {}
685 _ => panic!("Expected Ext(Goodbye) command"),
686 }
687
688 let cli = Cli::<
690 EthereumChainSpecParser,
691 NoArgs,
692 DefaultRpcModuleValidator,
693 CustomCommands,
694 >::try_parse_from(["reth", "node"])
695 .unwrap();
696
697 match &cli.command {
698 Commands::Node(_) => {}
699 _ => panic!("Expected Node command"),
700 }
701
702 let cli = Cli::<
704 EthereumChainSpecParser,
705 NoArgs,
706 DefaultRpcModuleValidator,
707 CustomCommands,
708 >::try_parse_from(["reth", "hello", "--name", "world"])
709 .unwrap();
710
711 if let Commands::Ext(cmd) = cli.command {
712 let runner = CliRunner::try_default_runtime().unwrap();
713 cmd.execute(runner).unwrap();
714 assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
715 }
716 }
717}