reth_ethereum_cli/
interface.rs

1//! CLI definition and entrypoint to executable
2
3use 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, CliHeader, CliNodeTypes},
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, 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;
28use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
29use tracing::info;
30
31/// The main reth cli interface.
32///
33/// This is the entrypoint to the executable.
34#[derive(Debug, Parser)]
35#[command(author, 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    /// The command to run
42    #[command(subcommand)]
43    pub command: Commands<C, Ext>,
44
45    /// The logging configuration for the CLI.
46    #[command(flatten)]
47    pub logs: LogArgs,
48
49    /// The tracing configuration for the CLI.
50    #[command(flatten)]
51    pub traces: TraceArgs,
52
53    /// Type marker for the RPC module validator
54    #[arg(skip)]
55    pub _phantom: PhantomData<Rpc>,
56}
57
58impl Cli {
59    /// Parsers only the default CLI arguments
60    pub fn parse_args() -> Self {
61        Self::parse()
62    }
63
64    /// Parsers only the default CLI arguments from the given iterator
65    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    /// Configures the CLI and returns a [`CliApp`] instance.
76    ///
77    /// This method is used to prepare the CLI for execution by wrapping it in a
78    /// [`CliApp`] that can be further configured before running.
79    pub fn configure(self) -> CliApp<C, Ext, Rpc>
80    where
81        C: ChainSpecParser<ChainSpec = ChainSpec>,
82    {
83        CliApp::new(self)
84    }
85
86    /// Execute the configured cli command.
87    ///
88    /// This accepts a closure that is used to launch the node via the
89    /// [`NodeCommand`](node::NodeCommand).
90    ///
91    /// This command will be run on the [default tokio runtime](reth_cli_runner::tokio_runtime).
92    ///
93    ///
94    /// # Example
95    ///
96    /// ```no_run
97    /// use reth_ethereum_cli::interface::Cli;
98    /// use reth_node_ethereum::EthereumNode;
99    ///
100    /// Cli::parse_args()
101    ///     .run(async move |builder, _| {
102    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
103    ///
104    ///         handle.wait_for_node_exit().await
105    ///     })
106    ///     .unwrap();
107    /// ```
108    ///
109    /// # Example
110    ///
111    /// Parse additional CLI arguments for the node command and use it to configure the node.
112    ///
113    /// ```no_run
114    /// use clap::Parser;
115    /// use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, interface::Cli};
116    ///
117    /// #[derive(Debug, Parser)]
118    /// pub struct MyArgs {
119    ///     pub enable: bool,
120    /// }
121    ///
122    /// Cli::<EthereumChainSpecParser, MyArgs>::parse()
123    ///     .run(async move |builder, my_args: MyArgs|
124    ///         // launch the node
125    ///         Ok(()))
126    ///     .unwrap();
127    /// ````
128    pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
129    where
130        L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
131        Fut: Future<Output = eyre::Result<()>>,
132        C: ChainSpecParser<ChainSpec = ChainSpec>,
133    {
134        self.with_runner(CliRunner::try_default_runtime()?, launcher)
135    }
136
137    /// Execute the configured cli command with the provided [`CliComponentsBuilder`].
138    ///
139    /// This accepts a closure that is used to launch the node via the
140    /// [`NodeCommand`](node::NodeCommand).
141    ///
142    /// This command will be run on the [default tokio runtime](reth_cli_runner::tokio_runtime).
143    pub fn run_with_components<N>(
144        self,
145        components: impl CliComponentsBuilder<N>,
146        launcher: impl AsyncFnOnce(
147            WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
148            Ext,
149        ) -> eyre::Result<()>,
150    ) -> eyre::Result<()>
151    where
152        N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
153        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
154    {
155        self.with_runner_and_components(CliRunner::try_default_runtime()?, components, launcher)
156    }
157
158    /// Execute the configured cli command with the provided [`CliRunner`].
159    ///
160    ///
161    /// # Example
162    ///
163    /// ```no_run
164    /// use reth_cli_runner::CliRunner;
165    /// use reth_ethereum_cli::interface::Cli;
166    /// use reth_node_ethereum::EthereumNode;
167    ///
168    /// let runner = CliRunner::try_default_runtime().unwrap();
169    ///
170    /// Cli::parse_args()
171    ///     .with_runner(runner, |builder, _| async move {
172    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
173    ///         handle.wait_for_node_exit().await
174    ///     })
175    ///     .unwrap();
176    /// ```
177    pub fn with_runner<L, Fut>(self, runner: CliRunner, launcher: L) -> eyre::Result<()>
178    where
179        L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
180        Fut: Future<Output = eyre::Result<()>>,
181        C: ChainSpecParser<ChainSpec = ChainSpec>,
182    {
183        let mut app = self.configure();
184        app.set_runner(runner);
185        app.run(FnLauncher::new::<C, Ext>(async move |builder, ext| launcher(builder, ext).await))
186    }
187
188    /// Execute the configured cli command with the provided [`CliRunner`] and
189    /// [`CliComponentsBuilder`].
190    pub fn with_runner_and_components<N>(
191        mut self,
192        runner: CliRunner,
193        components: impl CliComponentsBuilder<N>,
194        launcher: impl AsyncFnOnce(
195            WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>,
196            Ext,
197        ) -> eyre::Result<()>,
198    ) -> eyre::Result<()>
199    where
200        N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: CliHeader>, ChainSpec: Hardforks>,
201        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
202    {
203        // Add network name if available to the logs dir
204        if let Some(chain_spec) = self.command.chain_spec() {
205            self.logs.log_file_directory =
206                self.logs.log_file_directory.join(chain_spec.chain().to_string());
207        }
208        let _guard = self.init_tracing()?;
209        info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
210
211        // Install the prometheus recorder to be sure to record all metrics
212        let _ = install_prometheus_recorder();
213
214        // Use the shared standalone function to avoid duplication
215        run_commands_with::<C, Ext, Rpc, N>(self, runner, components, launcher)
216    }
217
218    /// Initializes tracing with the configured options.
219    ///
220    /// If file logging is enabled, this function returns a guard that must be kept alive to ensure
221    /// that all logs are flushed to disk.
222    /// If an OTLP endpoint is specified, it will export metrics to the configured collector.
223    pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
224        let layers = reth_tracing::Layers::new();
225
226        let guard = self.logs.init_tracing_with_layers(layers)?;
227        Ok(guard)
228    }
229}
230
231/// Commands to be executed
232#[derive(Debug, Subcommand)]
233pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
234    /// Start the node
235    #[command(name = "node")]
236    Node(Box<node::NodeCommand<C, Ext>>),
237    /// Initialize the database from a genesis file.
238    #[command(name = "init")]
239    Init(init_cmd::InitCommand<C>),
240    /// Initialize the database from a state dump file.
241    #[command(name = "init-state")]
242    InitState(init_state::InitStateCommand<C>),
243    /// This syncs RLP encoded blocks from a file or files.
244    #[command(name = "import")]
245    Import(import::ImportCommand<C>),
246    /// This syncs ERA encoded blocks from a directory.
247    #[command(name = "import-era")]
248    ImportEra(import_era::ImportEraCommand<C>),
249    /// Exports block to era1 files in a specified directory.
250    #[command(name = "export-era")]
251    ExportEra(export_era::ExportEraCommand<C>),
252    /// Dumps genesis block JSON configuration to stdout.
253    DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
254    /// Database debugging utilities
255    #[command(name = "db")]
256    Db(Box<db::Command<C>>),
257    /// Download public node snapshots
258    #[command(name = "download")]
259    Download(download::DownloadCommand<C>),
260    /// Manipulate individual stages.
261    #[command(name = "stage")]
262    Stage(stage::Command<C>),
263    /// P2P Debugging utilities
264    #[command(name = "p2p")]
265    P2P(Box<p2p::Command<C>>),
266    /// Generate Test Vectors
267    #[cfg(feature = "dev")]
268    #[command(name = "test-vectors")]
269    TestVectors(reth_cli_commands::test_vectors::Command),
270    /// Write config to stdout
271    #[command(name = "config")]
272    Config(config_cmd::Command),
273    /// Prune according to the configuration without any limits
274    #[command(name = "prune")]
275    Prune(prune::PruneCommand<C>),
276    /// Re-execute blocks in parallel to verify historical sync correctness.
277    #[command(name = "re-execute")]
278    ReExecute(re_execute::Command<C>),
279}
280
281impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
282    /// Returns the underlying chain being used for commands
283    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
284        match self {
285            Self::Node(cmd) => cmd.chain_spec(),
286            Self::Init(cmd) => cmd.chain_spec(),
287            Self::InitState(cmd) => cmd.chain_spec(),
288            Self::Import(cmd) => cmd.chain_spec(),
289            Self::ExportEra(cmd) => cmd.chain_spec(),
290            Self::ImportEra(cmd) => cmd.chain_spec(),
291            Self::DumpGenesis(cmd) => cmd.chain_spec(),
292            Self::Db(cmd) => cmd.chain_spec(),
293            Self::Download(cmd) => cmd.chain_spec(),
294            Self::Stage(cmd) => cmd.chain_spec(),
295            Self::P2P(cmd) => cmd.chain_spec(),
296            #[cfg(feature = "dev")]
297            Self::TestVectors(_) => None,
298            Self::Config(_) => None,
299            Self::Prune(cmd) => cmd.chain_spec(),
300            Self::ReExecute(cmd) => cmd.chain_spec(),
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::chainspec::SUPPORTED_CHAINS;
309    use clap::CommandFactory;
310    use reth_chainspec::SEPOLIA;
311    use reth_node_core::args::ColorMode;
312
313    #[test]
314    fn parse_color_mode() {
315        let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
316        assert_eq!(reth.logs.color, ColorMode::Always);
317    }
318
319    /// Tests that the help message is parsed correctly. This ensures that clap args are configured
320    /// correctly and no conflicts are introduced via attributes that would result in a panic at
321    /// runtime
322    #[test]
323    fn test_parse_help_all_subcommands() {
324        let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
325        for sub_command in reth.get_subcommands() {
326            let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
327                .err()
328                .unwrap_or_else(|| {
329                    panic!("Failed to parse help message {}", sub_command.get_name())
330                });
331
332            // --help is treated as error, but
333            // > Not a true "error" as it means --help or similar was used. The help message will be sent to stdout.
334            assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
335        }
336    }
337
338    /// Tests that the log directory is parsed correctly when using the node command. It's
339    /// always tied to the specific chain's name.
340    #[test]
341    fn parse_logs_path_node() {
342        let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
343        if let Some(chain_spec) = reth.command.chain_spec() {
344            reth.logs.log_file_directory =
345                reth.logs.log_file_directory.join(chain_spec.chain.to_string());
346        }
347        let log_dir = reth.logs.log_file_directory;
348        let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
349        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
350
351        let mut iter = SUPPORTED_CHAINS.iter();
352        iter.next();
353        for chain in iter {
354            let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
355            let chain =
356                reth.command.chain_spec().map(|c| c.chain.to_string()).unwrap_or(String::new());
357            reth.logs.log_file_directory = reth.logs.log_file_directory.join(chain.clone());
358            let log_dir = reth.logs.log_file_directory;
359            let end = format!("reth/logs/{chain}");
360            assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
361        }
362    }
363
364    /// Tests that the log directory is parsed correctly when using the init command. It
365    /// uses the underlying environment in command to get the chain.
366    #[test]
367    fn parse_logs_path_init() {
368        let mut reth = Cli::try_parse_args_from(["reth", "init"]).unwrap();
369        if let Some(chain_spec) = reth.command.chain_spec() {
370            reth.logs.log_file_directory =
371                reth.logs.log_file_directory.join(chain_spec.chain.to_string());
372        }
373        let log_dir = reth.logs.log_file_directory;
374        let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
375        println!("{log_dir:?}");
376        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
377    }
378
379    /// Tests that the config command does not return any chain spec leading to empty chain id.
380    #[test]
381    fn parse_empty_logs_path() {
382        let mut reth = Cli::try_parse_args_from(["reth", "config"]).unwrap();
383        if let Some(chain_spec) = reth.command.chain_spec() {
384            reth.logs.log_file_directory =
385                reth.logs.log_file_directory.join(chain_spec.chain.to_string());
386        }
387        let log_dir = reth.logs.log_file_directory;
388        let end = "reth/logs".to_string();
389        println!("{log_dir:?}");
390        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
391    }
392
393    #[test]
394    fn parse_env_filter_directives() {
395        let temp_dir = tempfile::tempdir().unwrap();
396
397        unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
398        let reth = Cli::try_parse_args_from([
399            "reth",
400            "init",
401            "--datadir",
402            temp_dir.path().to_str().unwrap(),
403            "--log.file.filter",
404            "debug,net=trace",
405        ])
406        .unwrap();
407        assert!(reth.run(async move |_, _| Ok(())).is_ok());
408    }
409
410    #[test]
411    fn test_rpc_module_validation() {
412        use reth_rpc_server_types::RethRpcModule;
413
414        // Test that standard modules are accepted
415        let cli =
416            Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
417
418        if let Commands::Node(command) = &cli.command {
419            if let Some(http_api) = &command.rpc.http_api {
420                // Should contain the expected modules
421                let modules = http_api.to_selection();
422                assert!(modules.contains(&RethRpcModule::Eth));
423                assert!(modules.contains(&RethRpcModule::Admin));
424                assert!(modules.contains(&RethRpcModule::Debug));
425            } else {
426                panic!("Expected http.api to be set");
427            }
428        } else {
429            panic!("Expected Node command");
430        }
431
432        // Test that unknown modules are parsed as Other variant
433        let cli =
434            Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
435
436        if let Commands::Node(command) = &cli.command {
437            if let Some(http_api) = &command.rpc.http_api {
438                let modules = http_api.to_selection();
439                assert!(modules.contains(&RethRpcModule::Eth));
440                assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
441            } else {
442                panic!("Expected http.api to be set");
443            }
444        } else {
445            panic!("Expected Node command");
446        }
447    }
448
449    #[test]
450    fn test_rpc_module_unknown_rejected() {
451        use reth_cli_runner::CliRunner;
452
453        // Test that unknown module names are rejected during validation
454        let cli =
455            Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
456
457        // When we try to run the CLI with validation, it should fail
458        let runner = CliRunner::try_default_runtime().unwrap();
459        let result = cli.with_runner(runner, |_, _| async { Ok(()) });
460
461        assert!(result.is_err());
462        let err = result.unwrap_err();
463        let err_msg = err.to_string();
464
465        // The error should mention it's an unknown module
466        assert!(
467            err_msg.contains("Unknown RPC module"),
468            "Error should mention unknown module: {}",
469            err_msg
470        );
471        assert!(
472            err_msg.contains("'unknownmodule'"),
473            "Error should mention the module name: {}",
474            err_msg
475        );
476    }
477
478    #[test]
479    fn parse_unwind_chain() {
480        let cli = Cli::try_parse_args_from([
481            "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
482        ])
483        .unwrap();
484        match cli.command {
485            Commands::Stage(cmd) => match cmd.command {
486                stage::Subcommands::Unwind(cmd) => {
487                    assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
488                }
489                _ => panic!("Expected Unwind command"),
490            },
491            _ => panic!("Expected Stage command"),
492        };
493    }
494
495    #[test]
496    fn parse_empty_supported_chains() {
497        #[derive(Debug, Clone, Default)]
498        struct FileChainSpecParser;
499
500        impl ChainSpecParser for FileChainSpecParser {
501            type ChainSpec = ChainSpec;
502
503            const SUPPORTED_CHAINS: &'static [&'static str] = &[];
504
505            fn parse(s: &str) -> eyre::Result<Arc<Self::ChainSpec>> {
506                EthereumChainSpecParser::parse(s)
507            }
508        }
509
510        let cli = Cli::<FileChainSpecParser>::try_parse_from([
511            "reth", "stage", "unwind", "--chain", "sepolia", "to-block", "100",
512        ])
513        .unwrap();
514        match cli.command {
515            Commands::Stage(cmd) => match cmd.command {
516                stage::Subcommands::Unwind(cmd) => {
517                    assert_eq!(cmd.chain_spec().unwrap().chain_id(), SEPOLIA.chain_id());
518                }
519                _ => panic!("Expected Unwind command"),
520            },
521            _ => panic!("Expected Stage command"),
522        };
523    }
524}