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, OtlpLogsStatus, 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 SubCmd: Subcommand + fmt::Debug = NoSubCmd,
41> {
42 #[command(subcommand)]
44 pub command: Commands<C, Ext, SubCmd>,
45
46 #[command(flatten)]
48 pub logs: LogArgs,
49
50 #[command(flatten)]
52 pub traces: TraceArgs,
53
54 #[arg(skip)]
56 pub _phantom: PhantomData<Rpc>,
57}
58
59impl Cli {
60 pub fn parse_args() -> Self {
62 Self::parse()
63 }
64
65 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
67 where
68 I: IntoIterator<Item = T>,
69 T: Into<OsString> + Clone,
70 {
71 Self::try_parse_from(itr)
72 }
73}
74
75impl<
76 C: ChainSpecParser,
77 Ext: clap::Args + fmt::Debug,
78 Rpc: RpcModuleValidator,
79 SubCmd: crate::app::ExtendedCommand + Subcommand + fmt::Debug,
80 > Cli<C, Ext, Rpc, SubCmd>
81{
82 pub fn configure(self) -> CliApp<C, Ext, Rpc, SubCmd> {
87 CliApp::new(self)
88 }
89
90 pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
133 where
134 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
135 Fut: Future<Output = eyre::Result<()>>,
136 C: ChainSpecParser<ChainSpec = ChainSpec>,
137 {
138 self.with_runner(CliRunner::try_default_runtime()?, launcher)
139 }
140
141 pub fn run_with_components<N>(
148 self,
149 components: impl CliComponentsBuilder<N>,
150 launcher: impl AsyncFnOnce(
151 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
152 Ext,
153 ) -> eyre::Result<()>,
154 ) -> eyre::Result<()>
155 where
156 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
157 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
158 {
159 self.with_runner_and_components(CliRunner::try_default_runtime()?, components, launcher)
160 }
161
162 pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
182 where
183 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
184 Fut: Future<Output = eyre::Result<()>>,
185 C: ChainSpecParser<ChainSpec = ChainSpec>,
186 {
187 let mut app = self.configure();
188 app.set_runner(runner);
189 app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
190 }
191
192 pub fn with_runner_and_components<N>(
195 mut self,
196 runner: CliRunner,
197 components: impl CliComponentsBuilder<N>,
198 launcher: impl AsyncFnOnce(
199 WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
200 Ext,
201 ) -> eyre::Result<()>,
202 ) -> eyre::Result<()>
203 where
204 N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
205 C: ChainSpecParser<ChainSpec = N::ChainSpec>,
206 {
207 if let Some(chain_spec) = self.command.chain_spec() {
209 self.logs.log_file_directory =
210 self.logs.log_file_directory.join(chain_spec.chain().to_string());
211 }
212 let _guard = self.init_tracing(&runner, Layers::new())?;
213
214 install_prometheus_recorder();
216
217 run_commands_with::<C, Ext, Rpc, N, SubCmd>(self, runner, components, launcher)
219 }
220
221 pub fn init_tracing(
229 &mut self,
230 runner: &CliRunner,
231 mut layers: Layers,
232 ) -> eyre::Result<Option<FileWorkerGuard>> {
233 let otlp_status = runner.block_on(self.traces.init_otlp_tracing(&mut layers))?;
234 let otlp_logs_status = runner.block_on(self.traces.init_otlp_logs(&mut layers))?;
235
236 let guard = self.logs.init_tracing_with_layers(layers)?;
237 info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
238
239 match otlp_status {
240 OtlpInitStatus::Started(endpoint) => {
241 info!(target: "reth::cli", "Started OTLP {:?} tracing export to {endpoint}", self.traces.protocol);
242 }
243 OtlpInitStatus::NoFeature => {
244 warn!(target: "reth::cli", "Provided OTLP tracing arguments do not have effect, compile with the `otlp` feature")
245 }
246 OtlpInitStatus::Disabled => {}
247 }
248
249 match otlp_logs_status {
250 OtlpLogsStatus::Started(endpoint) => {
251 info!(target: "reth::cli", "Started OTLP {:?} logs export to {endpoint}", self.traces.protocol);
252 }
253 OtlpLogsStatus::NoFeature => {
254 warn!(target: "reth::cli", "Provided OTLP logs arguments do not have effect, compile with the `otlp-logs` feature")
255 }
256 OtlpLogsStatus::Disabled => {}
257 }
258
259 Ok(guard)
260 }
261}
262
263#[derive(Debug, Subcommand)]
265pub enum Commands<
266 C: ChainSpecParser,
267 Ext: clap::Args + fmt::Debug,
268 SubCmd: Subcommand + fmt::Debug = NoSubCmd,
269> {
270 #[command(name = "node")]
272 Node(Box<node::NodeCommand<C, Ext>>),
273 #[command(name = "init")]
275 Init(init_cmd::InitCommand<C>),
276 #[command(name = "init-state")]
278 InitState(init_state::InitStateCommand<C>),
279 #[command(name = "import")]
281 Import(import::ImportCommand<C>),
282 #[command(name = "import-era")]
284 ImportEra(import_era::ImportEraCommand<C>),
285 #[command(name = "export-era")]
287 ExportEra(export_era::ExportEraCommand<C>),
288 DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
290 #[command(name = "db")]
292 Db(Box<db::Command<C>>),
293 #[command(name = "download")]
295 Download(download::DownloadCommand<C>),
296 #[command(name = "stage")]
298 Stage(stage::Command<C>),
299 #[command(name = "p2p")]
301 P2P(Box<p2p::Command<C>>),
302 #[cfg(feature = "dev")]
304 #[command(name = "test-vectors")]
305 TestVectors(reth_cli_commands::test_vectors::Command),
306 #[command(name = "config")]
308 Config(config_cmd::Command),
309 #[command(name = "prune")]
311 Prune(prune::PruneCommand<C>),
312 #[command(name = "re-execute")]
314 ReExecute(re_execute::Command<C>),
315 #[command(flatten)]
317 Ext(SubCmd),
318}
319
320#[derive(Debug, Subcommand)]
325pub enum NoSubCmd {}
326
327impl crate::app::ExtendedCommand for NoSubCmd {
328 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
329 match self {}
330 }
331}
332
333impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, SubCmd: Subcommand + fmt::Debug>
334 Commands<C, Ext, SubCmd>
335{
336 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
338 match self {
339 Self::Node(cmd) => cmd.chain_spec(),
340 Self::Init(cmd) => cmd.chain_spec(),
341 Self::InitState(cmd) => cmd.chain_spec(),
342 Self::Import(cmd) => cmd.chain_spec(),
343 Self::ExportEra(cmd) => cmd.chain_spec(),
344 Self::ImportEra(cmd) => cmd.chain_spec(),
345 Self::DumpGenesis(cmd) => cmd.chain_spec(),
346 Self::Db(cmd) => cmd.chain_spec(),
347 Self::Download(cmd) => cmd.chain_spec(),
348 Self::Stage(cmd) => cmd.chain_spec(),
349 Self::P2P(cmd) => cmd.chain_spec(),
350 #[cfg(feature = "dev")]
351 Self::TestVectors(_) => None,
352 Self::Config(_) => None,
353 Self::Prune(cmd) => cmd.chain_spec(),
354 Self::ReExecute(cmd) => cmd.chain_spec(),
355 Self::Ext(_) => None,
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::chainspec::SUPPORTED_CHAINS;
364 use clap::CommandFactory;
365 use reth_chainspec::SEPOLIA;
366 use reth_node_core::args::ColorMode;
367
368 #[test]
369 fn parse_color_mode() {
370 let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
371 assert_eq!(reth.logs.color, ColorMode::Always);
372 }
373
374 #[test]
378 fn test_parse_help_all_subcommands() {
379 let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
380 for sub_command in reth.get_subcommands() {
381 let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
382 .err()
383 .unwrap_or_else(|| {
384 panic!("Failed to parse help message {}", sub_command.get_name())
385 });
386
387 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
390 }
391 }
392
393 #[test]
396 fn parse_logs_path_node() {
397 let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
398 if let Some(chain_spec) = reth.command.chain_spec() {
399 reth.logs.log_file_directory =
400 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
401 }
402 let log_dir = reth.logs.log_file_directory;
403 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
404 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
405
406 let mut iter = SUPPORTED_CHAINS.iter();
407 iter.next();
408 for chain in iter {
409 let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
410 let chain =
411 reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
412 reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
413 let log_dir = reth.logs.log_file_directory;
414 let end = format!("reth/logs/{chain}");
415 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
416 }
417 }
418
419 #[test]
422 fn parse_logs_path_init() {
423 let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
424 if let Some(chain_spec) = reth.command.chain_spec() {
425 reth.logs.log_file_directory =
426 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
427 }
428 let log_dir = reth.logs.log_file_directory;
429 let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
430 println!("{log_dir:?}");
431 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
432 }
433
434 #[test]
436 fn parse_empty_logs_path() {
437 let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
438 if let Some(chain_spec) = reth.command.chain_spec() {
439 reth.logs.log_file_directory =
440 reth.logs.log_file_directory.join(chain_spec.chain.to_string());
441 }
442 let log_dir = reth.logs.log_file_directory;
443 let end = "reth/logs".to_string();
444 println!("{log_dir:?}");
445 assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
446 }
447
448 #[test]
449 fn parse_env_filter_directives() {
450 let temp_dir = tempfile::tempdir().unwrap();
451
452 unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
453 let reth = Cli::try_parse_args_from([
454 "reth",
455 "init",
456 "--datadir",
457 temp_dir.path().to_str().unwrap(),
458 "--log.file.filter",
459 "debug,net=trace",
460 ])
461 .unwrap();
462 assert!(reth.run(async move |_, _| Ok(())).is_ok());
463 }
464
465 #[test]
466 fn test_rpc_module_validation() {
467 use reth_rpc_server_types::RethRpcModule;
468
469 let cli =
471 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
472
473 if let Commands::Node(command) = &cli.command {
474 if let Some(http_api) = &command.rpc.http_api {
475 let modules = http_api.to_selection();
477 assert!(modules.contains(&RethRpcModule::Eth));
478 assert!(modules.contains(&RethRpcModule::Admin));
479 assert!(modules.contains(&RethRpcModule::Debug));
480 } else {
481 panic!("Expected http.api to be set");
482 }
483 } else {
484 panic!("Expected Node command");
485 }
486
487 let cli =
489 Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
490
491 if let Commands::Node(command) = &cli.command {
492 if let Some(http_api) = &command.rpc.http_api {
493 let modules = http_api.to_selection();
494 assert!(modules.contains(&RethRpcModule::Eth));
495 assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
496 } else {
497 panic!("Expected http.api to be set");
498 }
499 } else {
500 panic!("Expected Node command");
501 }
502 }
503
504 #[test]
505 fn test_rpc_module_unknown_rejected() {
506 use reth_cli_runner::CliRunner;
507
508 let cli =
510 Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
511
512 let runner = CliRunner::try_default_runtime().unwrap();
514 let result = cli.with_runner(runner, |_, _| async { Ok(()) });
515
516 assert!(result.is_err());
517 let err = result.unwrap_err();
518 let err_msg = err.to_string();
519
520 assert!(
522 err_msg.contains("Unknown RPC module"),
523 "Error should mention unknown module: {}",
524 err_msg
525 );
526 assert!(
527 err_msg.contains("'unknownmodule'"),
528 "Error should mention the module name: {}",
529 err_msg
530 );
531 }
532
533 #[test]
534 fn parse_unwind_chain() {
535 let cli = Cli::try_parse_args_from([
536 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
537 ])
538 .unwrap();
539 match cli.command {
540 Commands::Stage(cmd) => match cmd.command {
541 stage::Subcommands::Unwind(cmd) => {
542 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
543 }
544 _ => panic!("Expected Unwind command"),
545 },
546 _ => panic!("Expected Stage command"),
547 };
548 }
549
550 #[test]
551 fn parse_empty_supported_chains() {
552 #[derive(Debug, Clone, Default)]
553 struct FileChainSpecParser;
554
555 impl ChainSpecParser for FileChainSpecParser {
556 type ChainSpec = ChainSpec;
557
558 const SUPPORTED_CHAINS: &'static [&'static str] = &[];
559
560 fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
561 EthereumChainSpecParser::parse(s)
562 }
563 }
564
565 let cli = Cli::<FileChainSpecParser>::try_parse_from([
566 "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
567 ])
568 .unwrap();
569 match cli.command {
570 Commands::Stage(cmd) => match cmd.command {
571 stage::Subcommands::Unwind(cmd) => {
572 assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
573 }
574 _ => panic!("Expected Unwind command"),
575 },
576 _ => panic!("Expected Stage command"),
577 };
578 }
579
580 #[test]
581 fn test_extensible_subcommands() {
582 use crate::app::ExtendedCommand;
583 use reth_cli_runner::CliRunner;
584 use reth_rpc_server_types::DefaultRpcModuleValidator;
585 use std::sync::atomic::{AtomicBool, Ordering};
586
587 #[derive(Debug, Subcommand)]
588 enum CustomCommands {
589 #[command(name = "hello")]
591 Hello {
592 #[arg(long)]
594 name: String,
595 },
596 #[command(name = "goodbye")]
598 Goodbye,
599 }
600
601 static EXECUTED: AtomicBool = AtomicBool::new(false);
602
603 impl ExtendedCommand for CustomCommands {
604 fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
605 match self {
606 Self::Hello { name } => {
607 assert_eq!(name, "world");
608 EXECUTED.store(true, Ordering::SeqCst);
609 Ok(())
610 }
611 Self::Goodbye => Ok(()),
612 }
613 }
614 }
615
616 let cli = Cli::<
618 EthereumChainSpecParser,
619 NoArgs,
620 DefaultRpcModuleValidator,
621 CustomCommands,
622 >::try_parse_from(["reth", "hello", "--name", "world"])
623 .unwrap();
624
625 match &cli.command {
626 Commands::Ext(CustomCommands::Hello { name }) => {
627 assert_eq!(name, "world");
628 }
629 _ => panic!("Expected Ext(Hello) command"),
630 }
631
632 let cli = Cli::<
634 EthereumChainSpecParser,
635 NoArgs,
636 DefaultRpcModuleValidator,
637 CustomCommands,
638 >::try_parse_from(["reth", "goodbye"])
639 .unwrap();
640
641 match &cli.command {
642 Commands::Ext(CustomCommands::Goodbye) => {}
643 _ => panic!("Expected Ext(Goodbye) command"),
644 }
645
646 let cli = Cli::<
648 EthereumChainSpecParser,
649 NoArgs,
650 DefaultRpcModuleValidator,
651 CustomCommands,
652 >::try_parse_from(["reth", "node"])
653 .unwrap();
654
655 match &cli.command {
656 Commands::Node(_) => {}
657 _ => panic!("Expected Node command"),
658 }
659
660 let cli = Cli::<
662 EthereumChainSpecParser,
663 NoArgs,
664 DefaultRpcModuleValidator,
665 CustomCommands,
666 >::try_parse_from(["reth", "hello", "--name", "world"])
667 .unwrap();
668
669 if let Commands::Ext(cmd) = cli.command {
670 let runner = CliRunner::try_default_runtime().unwrap();
671 cmd.execute(runner).unwrap();
672 assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
673 }
674 }
675}