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