Skip to main content

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