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_node_builder::{NodeBuilder, WithLaunchContext};
20use reth_node_ethereum::{EthExecutorProvider, EthereumNode};
21use reth_node_metrics::recorder::install_prometheus_recorder;
22use reth_tracing::FileWorkerGuard;
23use std::{ffi::OsString, fmt, future::Future, sync::Arc};
24use tracing::info;
25
26/// Re-export of the `reth_node_core` types specifically in the `cli` module.
27///
28/// This is re-exported because the types in `reth_node_core::cli` originally existed in
29/// `reth::cli` but were moved to the `reth_node_core` crate. This re-export avoids a breaking
30/// change.
31pub use crate::core::cli::*;
32
33/// The main reth cli interface.
34///
35/// This is the entrypoint to the executable.
36#[derive(Debug, Parser)]
37#[command(author, version = SHORT_VERSION, long_version = LONG_VERSION, about = "Reth", long_about = None)]
38pub struct Cli<C: ChainSpecParser = EthereumChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs>
39{
40    /// The command to run
41    #[command(subcommand)]
42    pub command: Commands<C, Ext>,
43
44    /// The chain this node is running.
45    ///
46    /// Possible values are either a built-in chain or the path to a chain specification file.
47    #[arg(
48        long,
49        value_name = "CHAIN_OR_PATH",
50        long_help = C::help_message(),
51        default_value = C::SUPPORTED_CHAINS[0],
52        value_parser = C::parser(),
53        global = true,
54    )]
55    pub chain: Arc<C::ChainSpec>,
56
57    /// Add a new instance of a node.
58    ///
59    /// Configures the ports of the node to avoid conflicts with the defaults.
60    /// This is useful for running multiple nodes on the same machine.
61    ///
62    /// Max number of instances is 200. It is chosen in a way so that it's not possible to have
63    /// port numbers that conflict with each other.
64    ///
65    /// Changes to the following port numbers:
66    /// - `DISCOVERY_PORT`: default + `instance` - 1
67    /// - `AUTH_PORT`: default + `instance` * 100 - 100
68    /// - `HTTP_RPC_PORT`: default - `instance` + 1
69    /// - `WS_RPC_PORT`: default + `instance` * 2 - 2
70    #[arg(long, value_name = "INSTANCE", global = true, default_value_t = 1, value_parser = value_parser!(u16).range(..=200))]
71    pub instance: u16,
72
73    /// The logging configuration for the CLI.
74    #[command(flatten)]
75    pub logs: LogArgs,
76}
77
78impl Cli {
79    /// Parsers only the default CLI arguments
80    pub fn parse_args() -> Self {
81        Self::parse()
82    }
83
84    /// Parsers only the default CLI arguments from the given iterator
85    pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
86    where
87        I: IntoIterator<Item = T>,
88        T: Into<OsString> + Clone,
89    {
90        Self::try_parse_from(itr)
91    }
92}
93
94impl<C: ChainSpecParser<ChainSpec = ChainSpec>, Ext: clap::Args + fmt::Debug> Cli<C, Ext> {
95    /// Execute the configured cli command.
96    ///
97    /// This accepts a closure that is used to launch the node via the
98    /// [`NodeCommand`](node::NodeCommand).
99    ///
100    ///
101    /// # Example
102    ///
103    /// ```no_run
104    /// use reth::cli::Cli;
105    /// use reth_node_ethereum::EthereumNode;
106    ///
107    /// Cli::parse_args()
108    ///     .run(|builder, _| async move {
109    ///         let handle = builder.launch_node(EthereumNode::default()).await?;
110    ///
111    ///         handle.wait_for_node_exit().await
112    ///     })
113    ///     .unwrap();
114    /// ```
115    ///
116    /// # Example
117    ///
118    /// Parse additional CLI arguments for the node command and use it to configure the node.
119    ///
120    /// ```no_run
121    /// use clap::Parser;
122    /// use reth::cli::Cli;
123    /// use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
124    ///
125    /// #[derive(Debug, Parser)]
126    /// pub struct MyArgs {
127    ///     pub enable: bool,
128    /// }
129    ///
130    /// Cli::<EthereumChainSpecParser, MyArgs>::parse()
131    ///     .run(|builder, my_args: MyArgs| async move {
132    ///         // launch the node
133    ///
134    ///         Ok(())
135    ///     })
136    ///     .unwrap();
137    /// ````
138    pub fn run<L, Fut>(mut self, launcher: L) -> eyre::Result<()>
139    where
140        L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
141        Fut: Future<Output = eyre::Result<()>>,
142    {
143        // add network name to logs dir
144        self.logs.log_file_directory =
145            self.logs.log_file_directory.join(self.chain.chain.to_string());
146
147        let _guard = self.init_tracing()?;
148        info!(target: "reth::cli", "Initialized tracing, debug log directory: {}", self.logs.log_file_directory);
149
150        // Install the prometheus recorder to be sure to record all metrics
151        let _ = install_prometheus_recorder();
152
153        let runner = CliRunner::default();
154        match self.command {
155            Commands::Node(command) => {
156                runner.run_command_until_exit(|ctx| command.execute(ctx, 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) => runner.run_blocking_until_ctrl_c(
165                command.execute::<EthereumNode, _, _>(EthExecutorProvider::ethereum),
166            ),
167            Commands::DumpGenesis(command) => runner.run_blocking_until_ctrl_c(command.execute()),
168            Commands::Db(command) => {
169                runner.run_blocking_until_ctrl_c(command.execute::<EthereumNode>())
170            }
171            Commands::Stage(command) => runner.run_command_until_exit(|ctx| {
172                command.execute::<EthereumNode, _, _>(ctx, EthExecutorProvider::ethereum)
173            }),
174            Commands::P2P(command) => runner.run_until_ctrl_c(command.execute()),
175            #[cfg(feature = "dev")]
176            Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
177            Commands::Config(command) => runner.run_until_ctrl_c(command.execute()),
178            Commands::Debug(command) => {
179                runner.run_command_until_exit(|ctx| command.execute::<EthereumNode>(ctx))
180            }
181            Commands::Recover(command) => {
182                runner.run_command_until_exit(|ctx| command.execute::<EthereumNode>(ctx))
183            }
184            Commands::Prune(command) => runner.run_until_ctrl_c(command.execute::<EthereumNode>()),
185        }
186    }
187
188    /// Initializes tracing with the configured options.
189    ///
190    /// If file logging is enabled, this function returns a guard that must be kept alive to ensure
191    /// that all logs are flushed to disk.
192    pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
193        let guard = self.logs.init_tracing()?;
194        Ok(guard)
195    }
196}
197
198/// Commands to be executed
199#[derive(Debug, Subcommand)]
200pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
201    /// Start the node
202    #[command(name = "node")]
203    Node(Box<node::NodeCommand<C, Ext>>),
204    /// Initialize the database from a genesis file.
205    #[command(name = "init")]
206    Init(init_cmd::InitCommand<C>),
207    /// Initialize the database from a state dump file.
208    #[command(name = "init-state")]
209    InitState(init_state::InitStateCommand<C>),
210    /// This syncs RLP encoded blocks from a file.
211    #[command(name = "import")]
212    Import(import::ImportCommand<C>),
213    /// Dumps genesis block JSON configuration to stdout.
214    DumpGenesis(dump_genesis::DumpGenesisCommand<C>),
215    /// Database debugging utilities
216    #[command(name = "db")]
217    Db(db::Command<C>),
218    /// Manipulate individual stages.
219    #[command(name = "stage")]
220    Stage(stage::Command<C>),
221    /// P2P Debugging utilities
222    #[command(name = "p2p")]
223    P2P(p2p::Command<C>),
224    /// Generate Test Vectors
225    #[cfg(feature = "dev")]
226    #[command(name = "test-vectors")]
227    TestVectors(reth_cli_commands::test_vectors::Command),
228    /// Write config to stdout
229    #[command(name = "config")]
230    Config(config_cmd::Command),
231    /// Various debug routines
232    #[command(name = "debug")]
233    Debug(debug_cmd::Command<C>),
234    /// Scripts for node recovery
235    #[command(name = "recover")]
236    Recover(recover::Command<C>),
237    /// Prune according to the configuration without any limits
238    #[command(name = "prune")]
239    Prune(prune::PruneCommand<C>),
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::args::ColorMode;
246    use clap::CommandFactory;
247    use reth_ethereum_cli::chainspec::SUPPORTED_CHAINS;
248
249    #[test]
250    fn parse_color_mode() {
251        let reth = Cli::try_parse_args_from(["reth", "node", "--color", "always"]).unwrap();
252        assert_eq!(reth.logs.color, ColorMode::Always);
253    }
254
255    /// Tests that the help message is parsed correctly. This ensures that clap args are configured
256    /// correctly and no conflicts are introduced via attributes that would result in a panic at
257    /// runtime
258    #[test]
259    fn test_parse_help_all_subcommands() {
260        let reth = Cli::<EthereumChainSpecParser, NoArgs>::command();
261        for sub_command in reth.get_subcommands() {
262            let err = Cli::try_parse_args_from(["reth", sub_command.get_name(), "--help"])
263                .err()
264                .unwrap_or_else(|| {
265                    panic!("Failed to parse help message {}", sub_command.get_name())
266                });
267
268            // --help is treated as error, but
269            // > Not a true "error" as it means --help or similar was used. The help message will be sent to stdout.
270            assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
271        }
272    }
273
274    /// Tests that the log directory is parsed correctly. It's always tied to the specific chain's
275    /// name
276    #[test]
277    fn parse_logs_path() {
278        let mut reth = Cli::try_parse_args_from(["reth", "node"]).unwrap();
279        reth.logs.log_file_directory =
280            reth.logs.log_file_directory.join(reth.chain.chain.to_string());
281        let log_dir = reth.logs.log_file_directory;
282        let end = format!("reth/logs/{}", SUPPORTED_CHAINS[0]);
283        assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
284
285        let mut iter = SUPPORTED_CHAINS.iter();
286        iter.next();
287        for chain in iter {
288            let mut reth = Cli::try_parse_args_from(["reth", "node", "--chain", chain]).unwrap();
289            reth.logs.log_file_directory =
290                reth.logs.log_file_directory.join(reth.chain.chain.to_string());
291            let log_dir = reth.logs.log_file_directory;
292            let end = format!("reth/logs/{chain}");
293            assert!(log_dir.as_ref().ends_with(end), "{log_dir:?}");
294        }
295    }
296
297    #[test]
298    fn parse_env_filter_directives() {
299        let temp_dir = tempfile::tempdir().unwrap();
300
301        std::env::set_var("RUST_LOG", "info,evm=debug");
302        let reth = Cli::try_parse_args_from([
303            "reth",
304            "init",
305            "--datadir",
306            temp_dir.path().to_str().unwrap(),
307            "--log.file.filter",
308            "debug,net=trace",
309        ])
310        .unwrap();
311        assert!(reth.run(|_, _| async move { Ok(()) }).is_ok());
312    }
313}