reth/cli/
mod.rs

1//! CLI definition and entrypoint to executable
2
3use crate::{
4    args::LogArgs,
5    commands::debug_cmd,
6    version::{LONG_VERSION, SHORT_VERSION},
7};
8use clap::{value_parser, Parser, Subcommand};
9use reth_chainspec::ChainSpec;
10use reth_cli::chainspec::ChainSpecParser;
11use reth_cli_commands::{
12    config_cmd, db, dump_genesis, import, init_cmd, init_state,
13    node::{self, NoArgs},
14    p2p, prune, recover, stage,
15};
16use reth_cli_runner::CliRunner;
17use reth_db::DatabaseEnv;
18use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
19use reth_network::EthNetworkPrimitives;
20use reth_node_builder::{NodeBuilder, WithLaunchContext};
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/// Re-export of the `reth_node_core` types specifically in the `cli` module.
28///
29/// This is re-exported because the types in `reth_node_core::cli` originally existed in
30/// `reth::cli` but were moved to the `reth_node_core` crate. This re-export avoids a breaking
31/// change.
32pub use crate::core::cli::*;
33
34/// The main reth cli interface.
35///
36/// This is the entrypoint to the executable.
37#[derive(Debug, Parser)]
38#[command(author, version = SHORT_VERSION, long_version = LONG_VERSION, about = "Reth", long_about = None)]
39pub struct Cli<C: ChainSpecParser = EthereumChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs>
40{
41    /// The command to run
42    #[command(subcommand)]
43    pub command: Commands<C, Ext>,
44
45    /// The chain this node is running.
46    ///
47    /// Possible values are either a built-in chain or the path to a chain specification file.
48    #[arg(
49        long,
50        value_name = "CHAIN_OR_PATH",
51        long_help = C::help_message(),
52        default_value = C::SUPPORTED_CHAINS[0],
53        value_parser = C::parser(),
54        global = true,
55    )]
56    pub chain: Arc<C::ChainSpec>,
57
58    /// Add a new instance of a node.
59    ///
60    /// Configures the ports of the node to avoid conflicts with the defaults.
61    /// This is useful for running multiple nodes on the same machine.
62    ///
63    /// Max number of instances is 200. It is chosen in a way so that it's not possible to have
64    /// port numbers that conflict with each other.
65    ///
66    /// Changes to the following port numbers:
67    /// - `DISCOVERY_PORT`: default + `instance` - 1
68    /// - `AUTH_PORT`: default + `instance` * 100 - 100
69    /// - `HTTP_RPC_PORT`: default - `instance` + 1
70    /// - `WS_RPC_PORT`: default + `instance` * 2 - 2
71    #[arg(long, value_name = "INSTANCE", global = true, default_value_t = 1, value_parser = value_parser!(u16).range(..=200))]
72    pub instance: u16,
73
74    /// The logging configuration for the CLI.
75    #[command(flatten)]
76    pub logs: LogArgs,
77}
78
79impl Cli {
80    /// Parsers only the default CLI arguments
81    pub fn parse_args() -> Self {
82        Self::parse()
83    }
84
85    /// Parsers only the default CLI arguments from the given iterator
86    pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
87    where
88        I: IntoIterator<Item = T>,
89        T: Into<OsString> + Clone,
90    {
91        Self::try_parse_from(itr)
92    }
93}
94
95impl<C: ChainSpecParser<ChainSpec = ChainSpec>, Ext: clap::Args + fmt::Debug> Cli<C, Ext> {
96    /// Execute the configured cli command.
97    ///
98    /// This accepts a closure that is used to launch the node via the
99    /// [`NodeCommand`](node::NodeCommand).
100    ///
101    /// This command will be run on the [default tokio runtime](reth_cli_runner::tokio_runtime).
102    ///
103    ///
104    /// # Example
105    ///
106    /// ```no_run
107    /// use reth::cli::Cli;
108    /// use reth_node_ethereum::EthereumNode;
109    ///
110    /// Cli::parse_args()
111    ///     .run(async move |builder, _| {
112    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
113    ///
114    ///         handle.wait_for_node_exit().await
115    ///     })
116    ///     .unwrap();
117    /// ```
118    ///
119    /// # Example
120    ///
121    /// Parse additional CLI arguments for the node command and use it to configure the node.
122    ///
123    /// ```no_run
124    /// use clap::Parser;
125    /// use reth::cli::Cli;
126    /// use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
127    ///
128    /// #[derive(Debug, Parser)]
129    /// pub struct MyArgs {
130    ///     pub enable: bool,
131    /// }
132    ///
133    /// Cli::<EthereumChainSpecParser, MyArgs>::parse()
134    ///     .run(async move |builder, my_args: MyArgs|
135    ///         // launch the node
136    ///         Ok(()))
137    ///     .unwrap();
138    /// ````
139    pub fn run<L, Fut>(self, launcher: L) -> eyre::Result<()>
140    where
141        L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
142        Fut: Future<Output = eyre::Result<()>>,
143    {
144        self.with_runner(CliRunner::try_default_runtime()?, launcher)
145    }
146
147    /// Execute the configured cli command with the provided [`CliRunner`].
148    ///
149    ///
150    /// # Example
151    ///
152    /// ```no_run
153    /// use reth::cli::Cli;
154    /// use reth_cli_runner::CliRunner;
155    /// use reth_node_ethereum::EthereumNode;
156    ///
157    /// let runtime = tokio::runtime::Builder::new_multi_thread()
158    ///     .worker_threads(4)
159    ///     .max_blocking_threads(256)
160    ///     .enable_all()
161    ///     .build()
162    ///     .unwrap();
163    /// let runner = CliRunner::from_runtime(runtime);
164    ///
165    /// Cli::parse_args()
166    ///     .with_runner(runner, |builder, _| async move {
167    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
168    ///         handle.wait_for_node_exit().await
169    ///     })
170    ///     .unwrap();
171    /// ```
172    pub fn with_runner<L, Fut>(mut self, runner: CliRunner, launcher: L) -> eyre::Result<()>
173    where
174        L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
175        Fut: Future<Output = eyre::Result<()>>,
176    {
177        // add network name to logs dir
178        self.logs.log_file_directory =
179            self.logs.log_file_directory.join(self.chain.chain.to_string());
180
181        let _guard = self.init_tracing()?;
182        info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
183
184        // Install the prometheus recorder to be sure to record all metrics
185        let _ = install_prometheus_recorder();
186
187        let components = |spec: Arc<C::ChainSpec>| {
188            (EthExecutorProvider::ethereum(spec.clone()), EthBeaconConsensus::new(spec))
189        };
190        match self.command {
191            Commands::Node(command) => {
192                runner.run_command_until_exit(|ctx| command.execute(ctx, launcher))
193            }
194            Commands::Init(command) => {
195                runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
196            }
197            Commands::InitState(command) => {
198                runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
199            }
200            Commands::Import(command) => {
201                runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode, _, _>(components))
202            }
203            Commands::DumpGenesis(command) => runner.run_blocking_until_ctrl_c(command.execute()),
204            Commands::Db(command) => {
205                runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
206            }
207            Commands::Stage(command) => runner.run_command_until_exit(|ctx| {
208                command.execute::<EthereumNode, _, _, EthNetworkPrimitives>(ctx, components)
209            }),
210            Commands::P2P(command) => {
211                runner.run_until_ctrl_c(command.execute::<EthNetworkPrimitives>())
212            }
213            #[cfg(feature = "dev")]
214            Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
215            Commands::Config(command) => runner.run_until_ctrl_c(command.execute()),
216            Commands::Debug(command) => {
217                runner.run_command_until_exit(|ctx| command.execute::<EthereumNode>(ctx))
218            }
219            Commands::Recover(command) => {
220                runner.run_command_until_exit(|ctx| command.execute::<EthereumNode>(ctx))
221            }
222            Commands::Prune(command) => runner.run_until_ctrl_c(command.execute::<EthereumNode>()),
223        }
224    }
225
226    /// Initializes tracing with the configured options.
227    ///
228    /// If file logging is enabled, this function returns a guard that must be kept alive to ensure
229    /// that all logs are flushed to disk.
230    pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
231        let guard = self.logs.init_tracing()?;
232        Ok(guard)
233    }
234}
235
236/// Commands to be executed
237#[derive(Debug, Subcommand)]
238#[allow(clippy::large_enum_variant)]
239pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
240    /// Start the node
241    #[command(name = "node")]
242    Node(Box<node::NodeCommand<C, Ext>>),
243    /// Initialize the database from a genesis file.
244    #[command(name = "init")]
245    Init(init_cmd::InitCommand<C>),
246    /// Initialize the database from a state dump file.
247    #[command(name = "init-state")]
248    InitState(init_state::InitStateCommand<C>),
249    /// This syncs RLP encoded blocks from a file.
250    #[command(name = "import")]
251    Import(import::ImportCommand<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(db::Command<C>),
257    /// Manipulate individual stages.
258    #[command(name = "stage")]
259    Stage(stage::Command<C>),
260    /// P2P Debugging utilities
261    #[command(name = "p2p")]
262    P2P(p2p::Command<C>),
263    /// Generate Test Vectors
264    #[cfg(feature = "dev")]
265    #[command(name = "test-vectors")]
266    TestVectors(reth_cli_commands::test_vectors::Command),
267    /// Write config to stdout
268    #[command(name = "config")]
269    Config(config_cmd::Command),
270    /// Various debug routines
271    #[command(name = "debug")]
272    Debug(Box<debug_cmd::Command<C>>),
273    /// Scripts for node recovery
274    #[command(name = "recover")]
275    Recover(recover::Command<C>),
276    /// Prune according to the configuration without any limits
277    #[command(name = "prune")]
278    Prune(prune::PruneCommand<C>),
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::args::ColorMode;
285    use clap::CommandFactory;
286    use reth_ethereum_cli::chainspec::SUPPORTED_CHAINS;
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. It's always tied to the specific chain's
314    /// name
315    #[test]
316    fn parse_logs_path() {
317        let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
318        reth.logs.log_file_directory =
319            reth.logs.log_file_directory.join(reth.chain.chain.to_string());
320        let log_dir = reth.logs.log_file_directory;
321        let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
322        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
323
324        let mut iter = SUPPORTED_CHAINS.iter();
325        iter.next();
326        for chain in iter {
327            let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
328            reth.logs.log_file_directory =
329                reth.logs.log_file_directory.join(reth.chain.chain.to_string());
330            let log_dir = reth.logs.log_file_directory;
331            let end = format!("reth/logs/{chain}");
332            assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
333        }
334    }
335
336    #[test]
337    fn parse_env_filter_directives() {
338        let temp_dir = tempfile::tempdir().unwrap();
339
340        unsafe { std::env::set_var("RUST_LOG", "info,evm=debug") };
341        let reth = Cli::try_parse_args_from([
342            "reth",
343            "init",
344            "--datadir",
345            temp_dir.path().to_str().unwrap(),
346            "--log.file.filter",
347            "debug,net=trace",
348        ])
349        .unwrap();
350        assert!(reth.run(async move |_, _| Ok(())).is_ok());
351    }
352}