reth_cli_commands/
node.rs

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