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