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