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, StaticFilesArgs, 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    /// All static files related arguments
114    #[command(flatten, next_help_heading = "Static Files")]
115    pub static_files: StaticFilesArgs,
116
117    /// Additional cli arguments
118    #[command(flatten, next_help_heading = "Extension")]
119    pub ext: Ext,
120}
121
122impl<C: ChainSpecParser> NodeCommand<C> {
123    /// Parsers only the default CLI arguments
124    pub fn parse_args() -> Self {
125        Self::parse()
126    }
127
128    /// Parsers only the default [`NodeCommand`] arguments from the given iterator
129    pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
130    where
131        I: IntoIterator<Item = T>,
132        T: Into<OsString> + Clone,
133    {
134        Self::try_parse_from(itr)
135    }
136}
137
138impl<C, Ext> NodeCommand<C, Ext>
139where
140    C: ChainSpecParser,
141    C::ChainSpec: EthChainSpec + EthereumHardforks,
142    Ext: clap::Args + fmt::Debug,
143{
144    /// Launches the node
145    ///
146    /// This transforms the node command into a node config and launches the node using the given
147    /// launcher.
148    pub async fn execute<L>(self, ctx: CliContext, launcher: L) -> eyre::Result<()>
149    where
150        L: Launcher<C, Ext>,
151    {
152        tracing::info!(target: "reth::cli", version = ?version::version_metadata().short_version, "Starting {}",  version::version_metadata().name_client);
153
154        let Self {
155            datadir,
156            config,
157            chain,
158            metrics,
159            instance,
160            with_unused_ports,
161            network,
162            rpc,
163            txpool,
164            builder,
165            debug,
166            db,
167            dev,
168            pruning,
169            engine,
170            era,
171            static_files,
172            ext,
173        } = self;
174
175        // set up node config
176        let mut node_config = NodeConfig {
177            datadir,
178            config,
179            chain,
180            metrics,
181            instance,
182            network,
183            rpc,
184            txpool,
185            builder,
186            debug,
187            db,
188            dev,
189            pruning,
190            engine,
191            era,
192            static_files,
193        };
194
195        let data_dir = node_config.datadir();
196        let db_path = data_dir.db();
197
198        tracing::info!(target: "reth::cli", path = ?db_path, "Opening database");
199        let database = Arc::new(init_db(db_path.clone(), self.db.database_args())?.with_metrics());
200
201        if with_unused_ports {
202            node_config = node_config.with_unused_ports();
203        }
204
205        let builder = NodeBuilder::new(node_config)
206            .with_database(database)
207            .with_launch_context(ctx.task_executor);
208
209        launcher.entrypoint(builder, ext).await
210    }
211}
212
213impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> NodeCommand<C, Ext> {
214    /// Returns the underlying chain being used to run this command
215    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
216        Some(&self.chain)
217    }
218}
219
220/// No Additional arguments
221#[derive(Debug, Clone, Copy, Default, Args)]
222#[non_exhaustive]
223pub struct NoArgs;
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use reth_discv4::DEFAULT_DISCOVERY_PORT;
229    use reth_ethereum_cli::chainspec::{EthereumChainSpecParser, SUPPORTED_CHAINS};
230    use std::{
231        net::{IpAddr, Ipv4Addr, SocketAddr},
232        path::Path,
233    };
234
235    #[test]
236    fn parse_help_node_command() {
237        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from(["reth", "--help"])
238            .unwrap_err();
239        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
240    }
241
242    #[test]
243    fn parse_common_node_command_chain_args() {
244        for chain in SUPPORTED_CHAINS {
245            let args: NodeCommand<EthereumChainSpecParser> =
246                NodeCommand::parse_from(["reth", "--chain", chain]);
247            assert_eq!(args.chain.chain, chain.parse::<reth_chainspec::Chain>().unwrap());
248        }
249    }
250
251    #[test]
252    fn parse_discovery_addr() {
253        let cmd: NodeCommand<EthereumChainSpecParser> =
254            NodeCommand::try_parse_args_from(["reth", "--discovery.addr", "127.0.0.1"]).unwrap();
255        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
256    }
257
258    #[test]
259    fn parse_addr() {
260        let cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::try_parse_args_from([
261            "reth",
262            "--discovery.addr",
263            "127.0.0.1",
264            "--addr",
265            "127.0.0.1",
266        ])
267        .unwrap();
268        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
269        assert_eq!(cmd.network.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
270    }
271
272    #[test]
273    fn parse_discovery_port() {
274        let cmd: NodeCommand<EthereumChainSpecParser> =
275            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300"]).unwrap();
276        assert_eq!(cmd.network.discovery.port, 300);
277    }
278
279    #[test]
280    fn parse_port() {
281        let cmd: NodeCommand<EthereumChainSpecParser> =
282            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300", "--port", "99"])
283                .unwrap();
284        assert_eq!(cmd.network.discovery.port, 300);
285        assert_eq!(cmd.network.port, 99);
286    }
287
288    #[test]
289    fn parse_metrics_port() {
290        let cmd: NodeCommand<EthereumChainSpecParser> =
291            NodeCommand::try_parse_args_from(["reth", "--metrics", "9001"]).unwrap();
292        assert_eq!(
293            cmd.metrics.prometheus,
294            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
295        );
296
297        let cmd: NodeCommand<EthereumChainSpecParser> =
298            NodeCommand::try_parse_args_from(["reth", "--metrics", ":9001"]).unwrap();
299        assert_eq!(
300            cmd.metrics.prometheus,
301            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
302        );
303
304        let cmd: NodeCommand<EthereumChainSpecParser> =
305            NodeCommand::try_parse_args_from(["reth", "--metrics", "localhost:9001"]).unwrap();
306        assert_eq!(
307            cmd.metrics.prometheus,
308            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
309        );
310    }
311
312    #[test]
313    fn parse_config_path() {
314        let cmd: NodeCommand<EthereumChainSpecParser> =
315            NodeCommand::try_parse_args_from(["reth", "--config", "my/path/to/reth.toml"]).unwrap();
316        // always store reth.toml in the data dir, not the chain specific data dir
317        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
318        let config_path = cmd.config.unwrap_or_else(|| data_dir.config());
319        assert_eq!(config_path, Path::new("my/path/to/reth.toml"));
320
321        let cmd: NodeCommand<EthereumChainSpecParser> =
322            NodeCommand::try_parse_args_from(["reth"]).unwrap();
323
324        // always store reth.toml in the data dir, not the chain specific data dir
325        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
326        let config_path = cmd.config.clone().unwrap_or_else(|| data_dir.config());
327        let end = format!("{}/reth.toml", SUPPORTED_CHAINS[0]);
328        assert!(config_path.ends_with(end), "{:?}", cmd.config);
329    }
330
331    #[test]
332    fn parse_db_path() {
333        let cmd: NodeCommand<EthereumChainSpecParser> =
334            NodeCommand::try_parse_args_from(["reth"]).unwrap();
335        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
336
337        let db_path = data_dir.db();
338        let end = format!("reth/{}/db", SUPPORTED_CHAINS[0]);
339        assert!(db_path.ends_with(end), "{:?}", cmd.config);
340
341        let cmd: NodeCommand<EthereumChainSpecParser> =
342            NodeCommand::try_parse_args_from(["reth", "--datadir", "my/custom/path"]).unwrap();
343        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
344
345        let db_path = data_dir.db();
346        assert_eq!(db_path, Path::new("my/custom/path/db"));
347    }
348
349    #[test]
350    fn parse_instance() {
351        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
352        cmd.rpc.adjust_instance_ports(cmd.instance);
353        cmd.network.port = DEFAULT_DISCOVERY_PORT;
354        // check rpc port numbers
355        assert_eq!(cmd.rpc.auth_port, 8551);
356        assert_eq!(cmd.rpc.http_port, 8545);
357        assert_eq!(cmd.rpc.ws_port, 8546);
358        // check network listening port number
359        assert_eq!(cmd.network.port, 30303);
360
361        let mut cmd: NodeCommand<EthereumChainSpecParser> =
362            NodeCommand::parse_from(["reth", "--instance", "2"]);
363        cmd.rpc.adjust_instance_ports(cmd.instance);
364        cmd.network.port = DEFAULT_DISCOVERY_PORT + 2 - 1;
365        // check rpc port numbers
366        assert_eq!(cmd.rpc.auth_port, 8651);
367        assert_eq!(cmd.rpc.http_port, 8544);
368        assert_eq!(cmd.rpc.ws_port, 8548);
369        // check network listening port number
370        assert_eq!(cmd.network.port, 30304);
371
372        let mut cmd: NodeCommand<EthereumChainSpecParser> =
373            NodeCommand::parse_from(["reth", "--instance", "3"]);
374        cmd.rpc.adjust_instance_ports(cmd.instance);
375        cmd.network.port = DEFAULT_DISCOVERY_PORT + 3 - 1;
376        // check rpc port numbers
377        assert_eq!(cmd.rpc.auth_port, 8751);
378        assert_eq!(cmd.rpc.http_port, 8543);
379        assert_eq!(cmd.rpc.ws_port, 8550);
380        // check network listening port number
381        assert_eq!(cmd.network.port, 30305);
382    }
383
384    #[test]
385    fn parse_with_unused_ports() {
386        let cmd: NodeCommand<EthereumChainSpecParser> =
387            NodeCommand::parse_from(["reth", "--with-unused-ports"]);
388        assert!(cmd.with_unused_ports);
389    }
390
391    #[test]
392    fn with_unused_ports_conflicts_with_instance() {
393        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from([
394            "reth",
395            "--with-unused-ports",
396            "--instance",
397            "2",
398        ])
399        .unwrap_err();
400        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
401    }
402
403    #[test]
404    fn with_unused_ports_check_zero() {
405        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
406        cmd.rpc = cmd.rpc.with_unused_ports();
407        cmd.network = cmd.network.with_unused_ports();
408
409        // make sure the rpc ports are zero
410        assert_eq!(cmd.rpc.auth_port, 0);
411        assert_eq!(cmd.rpc.http_port, 0);
412        assert_eq!(cmd.rpc.ws_port, 0);
413
414        // make sure the network ports are zero
415        assert_eq!(cmd.network.port, 0);
416        assert_eq!(cmd.network.discovery.port, 0);
417
418        // make sure the ipc path is not the default
419        assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
420    }
421}