reth_cli_commands/
node.rs

1//! Main node command for launching a node
2
3use crate::launcher::Launcher;
4use clap::{value_parser, Args, Parser};
5use reth_chainspec::{EthChainSpec, EthereumHardforks};
6use reth_cli::chainspec::ChainSpecParser;
7use reth_cli_runner::CliContext;
8use reth_db::init_db;
9use reth_node_builder::NodeBuilder;
10use reth_node_core::{
11    args::{
12        DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, EraArgs, MetricArgs,
13        NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, TxPoolArgs,
14    },
15    node_config::NodeConfig,
16    version,
17};
18use std::{ffi::OsString, fmt, path::PathBuf, sync::Arc};
19
20/// Start the node
21#[derive(Debug, Parser)]
22pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs> {
23    /// The path to the configuration file to use.
24    #[arg(long, value_name = "FILE", verbatim_doc_comment)]
25    pub config: Option<PathBuf>,
26
27    /// The chain this node is running.
28    ///
29    /// Possible values are either a built-in chain or the path to a chain specification file.
30    #[arg(
31        long,
32        value_name = "CHAIN_OR_PATH",
33        long_help = C::help_message(),
34        default_value = C::default_value(),
35        default_value_if("dev", "true", "dev"),
36        value_parser = C::parser(),
37        required = false,
38    )]
39    pub chain: Arc<C::ChainSpec>,
40
41    /// Prometheus metrics configuration.
42    #[command(flatten)]
43    pub metrics: MetricArgs,
44
45    /// Add a new instance of a node.
46    ///
47    /// Configures the ports of the node to avoid conflicts with the defaults.
48    /// This is useful for running multiple nodes on the same machine.
49    ///
50    /// Max number of instances is 200. It is chosen in a way so that it's not possible to have
51    /// port numbers that conflict with each other.
52    ///
53    /// Changes to the following port numbers:
54    /// - `DISCOVERY_PORT`: default + `instance` - 1
55    /// - `AUTH_PORT`: default + `instance` * 100 - 100
56    /// - `HTTP_RPC_PORT`: default - `instance` + 1
57    /// - `WS_RPC_PORT`: default + `instance` * 2 - 2
58    /// - `IPC_PATH`: default + `-instance`
59    #[arg(long, value_name = "INSTANCE", global = true, value_parser = value_parser!(u16).range(1..=200))]
60    pub instance: Option<u16>,
61
62    /// Sets all ports to unused, allowing the OS to choose random unused ports when sockets are
63    /// bound.
64    ///
65    /// Mutually exclusive with `--instance`.
66    #[arg(long, conflicts_with = "instance", global = true)]
67    pub with_unused_ports: bool,
68
69    /// All datadir related arguments
70    #[command(flatten)]
71    pub datadir: DatadirArgs,
72
73    /// All networking related arguments
74    #[command(flatten)]
75    pub network: NetworkArgs,
76
77    /// All rpc related arguments
78    #[command(flatten)]
79    pub rpc: RpcServerArgs,
80
81    /// All txpool related arguments with --txpool prefix
82    #[command(flatten)]
83    pub txpool: TxPoolArgs,
84
85    /// All payload builder related arguments
86    #[command(flatten)]
87    pub builder: PayloadBuilderArgs,
88
89    /// All debug related arguments with --debug prefix
90    #[command(flatten)]
91    pub debug: DebugArgs,
92
93    /// All database related arguments
94    #[command(flatten)]
95    pub db: DatabaseArgs,
96
97    /// All dev related arguments with --dev prefix
98    #[command(flatten)]
99    pub dev: DevArgs,
100
101    /// All pruning related arguments
102    #[command(flatten)]
103    pub pruning: PruningArgs,
104
105    /// Engine cli arguments
106    #[command(flatten, next_help_heading = "Engine")]
107    pub engine: EngineArgs,
108
109    /// All ERA related arguments with --era prefix
110    #[command(flatten, next_help_heading = "ERA")]
111    pub era: EraArgs,
112
113    /// Additional cli arguments
114    #[command(flatten, next_help_heading = "Extension")]
115    pub ext: Ext,
116}
117
118impl<C: ChainSpecParser> NodeCommand<C> {
119    /// Parsers only the default CLI arguments
120    pub fn parse_args() -> Self {
121        Self::parse()
122    }
123
124    /// Parsers only the default [`NodeCommand`] arguments from the given iterator
125    pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
126    where
127        I: IntoIterator<Item = T>,
128        T: Into<OsString> + Clone,
129    {
130        Self::try_parse_from(itr)
131    }
132}
133
134impl<C, Ext> NodeCommand<C, Ext>
135where
136    C: ChainSpecParser,
137    C::ChainSpec: EthChainSpec + EthereumHardforks,
138    Ext: clap::Args + fmt::Debug,
139{
140    /// Launches the node
141    ///
142    /// This transforms the node command into a node config and launches the node using the given
143    /// launcher.
144    pub async fn execute<L>(self, ctx: CliContext, launcher: L) -> eyre::Result<()>
145    where
146        L: Launcher<C, Ext>,
147    {
148        tracing::info!(target: "reth::cli", version = ?version::version_metadata().short_version, "Starting reth");
149
150        let Self {
151            datadir,
152            config,
153            chain,
154            metrics,
155            instance,
156            with_unused_ports,
157            network,
158            rpc,
159            txpool,
160            builder,
161            debug,
162            db,
163            dev,
164            pruning,
165            ext,
166            engine,
167            era,
168        } = self;
169
170        // set up node config
171        let mut node_config = NodeConfig {
172            datadir,
173            config,
174            chain,
175            metrics,
176            instance,
177            network,
178            rpc,
179            txpool,
180            builder,
181            debug,
182            db,
183            dev,
184            pruning,
185            engine,
186            era,
187        };
188
189        let data_dir = node_config.datadir();
190        let db_path = data_dir.db();
191
192        tracing::info!(target: "reth::cli", path = ?db_path, "Opening database");
193        let database = Arc::new(init_db(db_path.clone(), self.db.database_args())?.with_metrics());
194
195        if with_unused_ports {
196            node_config = node_config.with_unused_ports();
197        }
198
199        let builder = NodeBuilder::new(node_config)
200            .with_database(database)
201            .with_launch_context(ctx.task_executor);
202
203        launcher.entrypoint(builder, ext).await
204    }
205}
206
207impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> NodeCommand<C, Ext> {
208    /// Returns the underlying chain being used to run this command
209    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
210        Some(&self.chain)
211    }
212}
213
214/// No Additional arguments
215#[derive(Debug, Clone, Copy, Default, Args)]
216#[non_exhaustive]
217pub struct NoArgs;
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use reth_discv4::DEFAULT_DISCOVERY_PORT;
223    use reth_ethereum_cli::chainspec::{EthereumChainSpecParser, SUPPORTED_CHAINS};
224    use std::{
225        net::{IpAddr, Ipv4Addr, SocketAddr},
226        path::Path,
227    };
228
229    #[test]
230    fn parse_help_node_command() {
231        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from(["reth", "--help"])
232            .unwrap_err();
233        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
234    }
235
236    #[test]
237    fn parse_common_node_command_chain_args() {
238        for chain in SUPPORTED_CHAINS {
239            let args: NodeCommand<EthereumChainSpecParser> =
240                NodeCommand::parse_from(["reth", "--chain", chain]);
241            assert_eq!(args.chain.chain, chain.parse::<reth_chainspec::Chain>().unwrap());
242        }
243    }
244
245    #[test]
246    fn parse_discovery_addr() {
247        let cmd: NodeCommand<EthereumChainSpecParser> =
248            NodeCommand::try_parse_args_from(["reth", "--discovery.addr", "127.0.0.1"]).unwrap();
249        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
250    }
251
252    #[test]
253    fn parse_addr() {
254        let cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::try_parse_args_from([
255            "reth",
256            "--discovery.addr",
257            "127.0.0.1",
258            "--addr",
259            "127.0.0.1",
260        ])
261        .unwrap();
262        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
263        assert_eq!(cmd.network.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
264    }
265
266    #[test]
267    fn parse_discovery_port() {
268        let cmd: NodeCommand<EthereumChainSpecParser> =
269            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300"]).unwrap();
270        assert_eq!(cmd.network.discovery.port, 300);
271    }
272
273    #[test]
274    fn parse_port() {
275        let cmd: NodeCommand<EthereumChainSpecParser> =
276            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300", "--port", "99"])
277                .unwrap();
278        assert_eq!(cmd.network.discovery.port, 300);
279        assert_eq!(cmd.network.port, 99);
280    }
281
282    #[test]
283    fn parse_metrics_port() {
284        let cmd: NodeCommand<EthereumChainSpecParser> =
285            NodeCommand::try_parse_args_from(["reth", "--metrics", "9001"]).unwrap();
286        assert_eq!(
287            cmd.metrics.prometheus,
288            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
289        );
290
291        let cmd: NodeCommand<EthereumChainSpecParser> =
292            NodeCommand::try_parse_args_from(["reth", "--metrics", ":9001"]).unwrap();
293        assert_eq!(
294            cmd.metrics.prometheus,
295            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
296        );
297
298        let cmd: NodeCommand<EthereumChainSpecParser> =
299            NodeCommand::try_parse_args_from(["reth", "--metrics", "localhost:9001"]).unwrap();
300        assert_eq!(
301            cmd.metrics.prometheus,
302            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
303        );
304    }
305
306    #[test]
307    fn parse_config_path() {
308        let cmd: NodeCommand<EthereumChainSpecParser> =
309            NodeCommand::try_parse_args_from(["reth", "--config", "my/path/to/reth.toml"]).unwrap();
310        // always store reth.toml in the data dir, not the chain specific data dir
311        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
312        let config_path = cmd.config.unwrap_or_else(|| data_dir.config());
313        assert_eq!(config_path, Path::new("my/path/to/reth.toml"));
314
315        let cmd: NodeCommand<EthereumChainSpecParser> =
316            NodeCommand::try_parse_args_from(["reth"]).unwrap();
317
318        // always store reth.toml in the data dir, not the chain specific data dir
319        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
320        let config_path = cmd.config.clone().unwrap_or_else(|| data_dir.config());
321        let end = format!("{}/reth.toml", SUPPORTED_CHAINS[0]);
322        assert!(config_path.ends_with(end), "{:?}", cmd.config);
323    }
324
325    #[test]
326    fn parse_db_path() {
327        let cmd: NodeCommand<EthereumChainSpecParser> =
328            NodeCommand::try_parse_args_from(["reth"]).unwrap();
329        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
330
331        let db_path = data_dir.db();
332        let end = format!("reth/{}/db", SUPPORTED_CHAINS[0]);
333        assert!(db_path.ends_with(end), "{:?}", cmd.config);
334
335        let cmd: NodeCommand<EthereumChainSpecParser> =
336            NodeCommand::try_parse_args_from(["reth", "--datadir", "my/custom/path"]).unwrap();
337        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
338
339        let db_path = data_dir.db();
340        assert_eq!(db_path, Path::new("my/custom/path/db"));
341    }
342
343    #[test]
344    fn parse_instance() {
345        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
346        cmd.rpc.adjust_instance_ports(cmd.instance);
347        cmd.network.port = DEFAULT_DISCOVERY_PORT;
348        // check rpc port numbers
349        assert_eq!(cmd.rpc.auth_port, 8551);
350        assert_eq!(cmd.rpc.http_port, 8545);
351        assert_eq!(cmd.rpc.ws_port, 8546);
352        // check network listening port number
353        assert_eq!(cmd.network.port, 30303);
354
355        let mut cmd: NodeCommand<EthereumChainSpecParser> =
356            NodeCommand::parse_from(["reth", "--instance", "2"]);
357        cmd.rpc.adjust_instance_ports(cmd.instance);
358        cmd.network.port = DEFAULT_DISCOVERY_PORT + 2 - 1;
359        // check rpc port numbers
360        assert_eq!(cmd.rpc.auth_port, 8651);
361        assert_eq!(cmd.rpc.http_port, 8544);
362        assert_eq!(cmd.rpc.ws_port, 8548);
363        // check network listening port number
364        assert_eq!(cmd.network.port, 30304);
365
366        let mut cmd: NodeCommand<EthereumChainSpecParser> =
367            NodeCommand::parse_from(["reth", "--instance", "3"]);
368        cmd.rpc.adjust_instance_ports(cmd.instance);
369        cmd.network.port = DEFAULT_DISCOVERY_PORT + 3 - 1;
370        // check rpc port numbers
371        assert_eq!(cmd.rpc.auth_port, 8751);
372        assert_eq!(cmd.rpc.http_port, 8543);
373        assert_eq!(cmd.rpc.ws_port, 8550);
374        // check network listening port number
375        assert_eq!(cmd.network.port, 30305);
376    }
377
378    #[test]
379    fn parse_with_unused_ports() {
380        let cmd: NodeCommand<EthereumChainSpecParser> =
381            NodeCommand::parse_from(["reth", "--with-unused-ports"]);
382        assert!(cmd.with_unused_ports);
383    }
384
385    #[test]
386    fn with_unused_ports_conflicts_with_instance() {
387        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from([
388            "reth",
389            "--with-unused-ports",
390            "--instance",
391            "2",
392        ])
393        .unwrap_err();
394        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
395    }
396
397    #[test]
398    fn with_unused_ports_check_zero() {
399        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
400        cmd.rpc = cmd.rpc.with_unused_ports();
401        cmd.network = cmd.network.with_unused_ports();
402
403        // make sure the rpc ports are zero
404        assert_eq!(cmd.rpc.auth_port, 0);
405        assert_eq!(cmd.rpc.http_port, 0);
406        assert_eq!(cmd.rpc.ws_port, 0);
407
408        // make sure the network ports are zero
409        assert_eq!(cmd.network.port, 0);
410        assert_eq!(cmd.network.discovery.port, 0);
411
412        // make sure the ipc path is not the default
413        assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
414    }
415}