reth_ethereum_cli/
interface.rs

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