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, EnclaveArgs, EngineArgs, 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::SUPPORTED_CHAINS[0],
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(..=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    /// Additional cli arguments
113    #[command(flatten, next_help_heading = "Extension")]
114    pub ext: Ext,
115
116    /// All enclave related arguments
117    #[command(flatten)]
118    pub enclave: EnclaveArgs,
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::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            enclave,
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            enclave,
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/// No Additional arguments
217#[derive(Debug, Clone, Copy, Default, Args)]
218#[non_exhaustive]
219pub struct NoArgs;
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use reth_discv4::DEFAULT_DISCOVERY_PORT;
225    use reth_ethereum_cli::chainspec::{EthereumChainSpecParser, SUPPORTED_CHAINS};
226    use std::{
227        net::{IpAddr, Ipv4Addr},
228        path::Path,
229    };
230
231    #[test]
232    fn parse_help_node_command() {
233        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from(["reth", "--help"])
234            .unwrap_err();
235        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
236    }
237
238    #[test]
239    fn parse_common_node_command_chain_args() {
240        for chain in SUPPORTED_CHAINS {
241            let args: NodeCommand<EthereumChainSpecParser> =
242                NodeCommand::parse_from(["reth", "--chain", chain]);
243            assert_eq!(args.chain.chain, chain.parse::<reth_chainspec::Chain>().unwrap());
244        }
245    }
246
247    #[test]
248    fn parse_discovery_addr() {
249        let cmd: NodeCommand<EthereumChainSpecParser> =
250            NodeCommand::try_parse_args_from(["reth", "--discovery.addr", "127.0.0.1"]).unwrap();
251        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
252    }
253
254    #[test]
255    fn parse_addr() {
256        let cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::try_parse_args_from([
257            "reth",
258            "--discovery.addr",
259            "127.0.0.1",
260            "--addr",
261            "127.0.0.1",
262        ])
263        .unwrap();
264        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
265        assert_eq!(cmd.network.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
266    }
267
268    #[test]
269    fn parse_discovery_port() {
270        let cmd: NodeCommand<EthereumChainSpecParser> =
271            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300"]).unwrap();
272        assert_eq!(cmd.network.discovery.port, 300);
273    }
274
275    #[test]
276    fn parse_port() {
277        let cmd: NodeCommand<EthereumChainSpecParser> =
278            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300", "--port", "99"])
279                .unwrap();
280        assert_eq!(cmd.network.discovery.port, 300);
281        assert_eq!(cmd.network.port, 99);
282    }
283
284    #[test]
285    fn parse_metrics_port() {
286        let cmd: NodeCommand<EthereumChainSpecParser> =
287            NodeCommand::try_parse_args_from(["reth", "--metrics", "9001"]).unwrap();
288        assert_eq!(cmd.metrics, Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001)));
289
290        let cmd: NodeCommand<EthereumChainSpecParser> =
291            NodeCommand::try_parse_args_from(["reth", "--metrics", ":9001"]).unwrap();
292        assert_eq!(cmd.metrics, Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001)));
293
294        let cmd: NodeCommand<EthereumChainSpecParser> =
295            NodeCommand::try_parse_args_from(["reth", "--metrics", "localhost:9001"]).unwrap();
296        assert_eq!(cmd.metrics, Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001)));
297    }
298
299    #[test]
300    fn parse_config_path() {
301        let cmd: NodeCommand<EthereumChainSpecParser> =
302            NodeCommand::try_parse_args_from(["reth", "--config", "my/path/to/reth.toml"]).unwrap();
303        // always store reth.toml in the data dir, not the chain specific data dir
304        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
305        let config_path = cmd.config.unwrap_or_else(|| data_dir.config());
306        assert_eq!(config_path, Path::new("my/path/to/reth.toml"));
307
308        let cmd: NodeCommand<EthereumChainSpecParser> =
309            NodeCommand::try_parse_args_from(["reth"]).unwrap();
310
311        // always store reth.toml in the data dir, not the chain specific data dir
312        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
313        let config_path = cmd.config.clone().unwrap_or_else(|| data_dir.config());
314        let end = format!("{}/reth.toml", SUPPORTED_CHAINS[0]);
315        assert!(config_path.ends_with(end), "{:?}", cmd.config);
316    }
317
318    #[test]
319    fn parse_db_path() {
320        let cmd: NodeCommand<EthereumChainSpecParser> =
321            NodeCommand::try_parse_args_from(["reth"]).unwrap();
322        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
323
324        let db_path = data_dir.db();
325        let end = format!("reth/{}/db", SUPPORTED_CHAINS[0]);
326        assert!(db_path.ends_with(end), "{:?}", cmd.config);
327
328        let cmd: NodeCommand<EthereumChainSpecParser> =
329            NodeCommand::try_parse_args_from(["reth", "--datadir", "my/custom/path"]).unwrap();
330        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
331
332        let db_path = data_dir.db();
333        assert_eq!(db_path, Path::new("my/custom/path/db"));
334    }
335
336    #[test]
337    fn parse_instance() {
338        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
339        cmd.rpc.adjust_instance_ports(cmd.instance);
340        cmd.network.port = DEFAULT_DISCOVERY_PORT;
341        // check rpc port numbers
342        assert_eq!(cmd.rpc.auth_port, 8551);
343        assert_eq!(cmd.rpc.http_port, 8545);
344        assert_eq!(cmd.rpc.ws_port, 8546);
345        // check network listening port number
346        assert_eq!(cmd.network.port, 30303);
347
348        let mut cmd: NodeCommand<EthereumChainSpecParser> =
349            NodeCommand::parse_from(["reth", "--instance", "2"]);
350        cmd.rpc.adjust_instance_ports(cmd.instance);
351        cmd.network.port = DEFAULT_DISCOVERY_PORT + 2 - 1;
352        // check rpc port numbers
353        assert_eq!(cmd.rpc.auth_port, 8651);
354        assert_eq!(cmd.rpc.http_port, 8544);
355        assert_eq!(cmd.rpc.ws_port, 8548);
356        // check network listening port number
357        assert_eq!(cmd.network.port, 30304);
358
359        let mut cmd: NodeCommand<EthereumChainSpecParser> =
360            NodeCommand::parse_from(["reth", "--instance", "3"]);
361        cmd.rpc.adjust_instance_ports(cmd.instance);
362        cmd.network.port = DEFAULT_DISCOVERY_PORT + 3 - 1;
363        // check rpc port numbers
364        assert_eq!(cmd.rpc.auth_port, 8751);
365        assert_eq!(cmd.rpc.http_port, 8543);
366        assert_eq!(cmd.rpc.ws_port, 8550);
367        // check network listening port number
368        assert_eq!(cmd.network.port, 30305);
369    }
370
371    #[test]
372    fn parse_with_unused_ports() {
373        let cmd: NodeCommand<EthereumChainSpecParser> =
374            NodeCommand::parse_from(["reth", "--with-unused-ports"]);
375        assert!(cmd.with_unused_ports);
376    }
377
378    #[test]
379    fn with_unused_ports_conflicts_with_instance() {
380        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from([
381            "reth",
382            "--with-unused-ports",
383            "--instance",
384            "2",
385        ])
386        .unwrap_err();
387        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
388    }
389
390    #[test]
391    fn with_unused_ports_check_zero() {
392        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
393        cmd.rpc = cmd.rpc.with_unused_ports();
394        cmd.network = cmd.network.with_unused_ports();
395
396        // make sure the rpc ports are zero
397        assert_eq!(cmd.rpc.auth_port, 0);
398        assert_eq!(cmd.rpc.http_port, 0);
399        assert_eq!(cmd.rpc.ws_port, 0);
400
401        // make sure the network ports are zero
402        assert_eq!(cmd.network.port, 0);
403        assert_eq!(cmd.network.discovery.port, 0);
404
405        // make sure the ipc path is not the default
406        assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
407    }
408}