reth_ethereum_cli/
interface.rs

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