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