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    /// Parsers only the default CLI arguments
129    pub fn parse_args() -> Self {
130        Self::parse()
131    }
132
133    /// Parsers 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        // set up node config
182        let mut node_config = NodeConfig {
183            datadir,
184            config,
185            chain,
186            metrics,
187            instance,
188            network,
189            rpc,
190            txpool,
191            builder,
192            debug,
193            db,
194            dev,
195            pruning,
196            engine,
197            era,
198            static_files,
199            storage,
200        };
201
202        let data_dir = node_config.datadir();
203        let db_path = data_dir.db();
204
205        tracing::info!(target: "reth::cli", path = ?db_path, "Opening database");
206        let database = init_db(db_path.clone(), self.db.database_args())?.with_metrics();
207
208        if with_unused_ports {
209            node_config = node_config.with_unused_ports();
210        }
211
212        let builder = NodeBuilder::new(node_config)
213            .with_database(database)
214            .with_launch_context(ctx.task_executor);
215
216        launcher.entrypoint(builder, ext).await
217    }
218}
219
220impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> NodeCommand<C, Ext> {
221    /// Returns the underlying chain being used to run this command
222    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
223        Some(&self.chain)
224    }
225}
226
227/// No Additional arguments
228#[derive(Debug, Clone, Copy, Default, Args)]
229#[non_exhaustive]
230pub struct NoArgs;
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use reth_discv4::DEFAULT_DISCOVERY_PORT;
236    use reth_ethereum_cli::chainspec::{EthereumChainSpecParser, SUPPORTED_CHAINS};
237    use std::{
238        net::{IpAddr, Ipv4Addr, SocketAddr},
239        path::Path,
240    };
241
242    #[test]
243    fn parse_help_node_command() {
244        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from(["reth", "--help"])
245            .unwrap_err();
246        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
247    }
248
249    #[test]
250    fn parse_common_node_command_chain_args() {
251        for chain in SUPPORTED_CHAINS {
252            let args: NodeCommand<EthereumChainSpecParser> =
253                NodeCommand::parse_from(["reth", "--chain", chain]);
254            assert_eq!(args.chain.chain, chain.parse::<reth_chainspec::Chain>().unwrap());
255        }
256    }
257
258    #[test]
259    fn parse_discovery_addr() {
260        let cmd: NodeCommand<EthereumChainSpecParser> =
261            NodeCommand::try_parse_args_from(["reth", "--discovery.addr", "127.0.0.1"]).unwrap();
262        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
263    }
264
265    #[test]
266    fn parse_addr() {
267        let cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::try_parse_args_from([
268            "reth",
269            "--discovery.addr",
270            "127.0.0.1",
271            "--addr",
272            "127.0.0.1",
273        ])
274        .unwrap();
275        assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
276        assert_eq!(cmd.network.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
277    }
278
279    #[test]
280    fn parse_discovery_port() {
281        let cmd: NodeCommand<EthereumChainSpecParser> =
282            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300"]).unwrap();
283        assert_eq!(cmd.network.discovery.port, 300);
284    }
285
286    #[test]
287    fn parse_port() {
288        let cmd: NodeCommand<EthereumChainSpecParser> =
289            NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300", "--port", "99"])
290                .unwrap();
291        assert_eq!(cmd.network.discovery.port, 300);
292        assert_eq!(cmd.network.port, 99);
293    }
294
295    #[test]
296    fn parse_metrics_port() {
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", ":9001"]).unwrap();
306        assert_eq!(
307            cmd.metrics.prometheus,
308            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
309        );
310
311        let cmd: NodeCommand<EthereumChainSpecParser> =
312            NodeCommand::try_parse_args_from(["reth", "--metrics", "localhost:9001"]).unwrap();
313        assert_eq!(
314            cmd.metrics.prometheus,
315            Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001))
316        );
317    }
318
319    #[test]
320    fn parse_config_path() {
321        let cmd: NodeCommand<EthereumChainSpecParser> =
322            NodeCommand::try_parse_args_from(["reth", "--config", "my/path/to/reth.toml"]).unwrap();
323        // always store reth.toml in the data dir, not the chain specific data dir
324        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
325        let config_path = cmd.config.unwrap_or_else(|| data_dir.config());
326        assert_eq!(config_path, Path::new("my/path/to/reth.toml"));
327
328        let cmd: NodeCommand<EthereumChainSpecParser> =
329            NodeCommand::try_parse_args_from(["reth"]).unwrap();
330
331        // always store reth.toml in the data dir, not the chain specific data dir
332        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
333        let config_path = cmd.config.clone().unwrap_or_else(|| data_dir.config());
334        let end = format!("{}/reth.toml", SUPPORTED_CHAINS[0]);
335        assert!(config_path.ends_with(end), "{:?}", cmd.config);
336    }
337
338    #[test]
339    fn parse_db_path() {
340        let cmd: NodeCommand<EthereumChainSpecParser> =
341            NodeCommand::try_parse_args_from(["reth"]).unwrap();
342        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
343
344        let db_path = data_dir.db();
345        let end = format!("reth/{}/db", SUPPORTED_CHAINS[0]);
346        assert!(db_path.ends_with(end), "{:?}", cmd.config);
347
348        let cmd: NodeCommand<EthereumChainSpecParser> =
349            NodeCommand::try_parse_args_from(["reth", "--datadir", "my/custom/path"]).unwrap();
350        let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
351
352        let db_path = data_dir.db();
353        assert_eq!(db_path, Path::new("my/custom/path/db"));
354    }
355
356    #[test]
357    fn parse_instance() {
358        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
359        cmd.rpc.adjust_instance_ports(cmd.instance);
360        cmd.network.port = DEFAULT_DISCOVERY_PORT;
361        // check rpc port numbers
362        assert_eq!(cmd.rpc.auth_port, 8551);
363        assert_eq!(cmd.rpc.http_port, 8545);
364        assert_eq!(cmd.rpc.ws_port, 8546);
365        // check network listening port number
366        assert_eq!(cmd.network.port, 30303);
367
368        let mut cmd: NodeCommand<EthereumChainSpecParser> =
369            NodeCommand::parse_from(["reth", "--instance", "2"]);
370        cmd.rpc.adjust_instance_ports(cmd.instance);
371        cmd.network.port = DEFAULT_DISCOVERY_PORT + 2 - 1;
372        // check rpc port numbers
373        assert_eq!(cmd.rpc.auth_port, 8651);
374        assert_eq!(cmd.rpc.http_port, 8544);
375        assert_eq!(cmd.rpc.ws_port, 8548);
376        // check network listening port number
377        assert_eq!(cmd.network.port, 30304);
378
379        let mut cmd: NodeCommand<EthereumChainSpecParser> =
380            NodeCommand::parse_from(["reth", "--instance", "3"]);
381        cmd.rpc.adjust_instance_ports(cmd.instance);
382        cmd.network.port = DEFAULT_DISCOVERY_PORT + 3 - 1;
383        // check rpc port numbers
384        assert_eq!(cmd.rpc.auth_port, 8751);
385        assert_eq!(cmd.rpc.http_port, 8543);
386        assert_eq!(cmd.rpc.ws_port, 8550);
387        // check network listening port number
388        assert_eq!(cmd.network.port, 30305);
389    }
390
391    #[test]
392    fn parse_with_unused_ports() {
393        let cmd: NodeCommand<EthereumChainSpecParser> =
394            NodeCommand::parse_from(["reth", "--with-unused-ports"]);
395        assert!(cmd.with_unused_ports);
396    }
397
398    #[test]
399    fn with_unused_ports_conflicts_with_instance() {
400        let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from([
401            "reth",
402            "--with-unused-ports",
403            "--instance",
404            "2",
405        ])
406        .unwrap_err();
407        assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
408    }
409
410    #[test]
411    fn with_unused_ports_check_zero() {
412        let mut cmd: NodeCommand<EthereumChainSpecParser> = NodeCommand::parse_from(["reth"]);
413        cmd.rpc = cmd.rpc.with_unused_ports();
414        cmd.network = cmd.network.with_unused_ports();
415
416        // make sure the rpc ports are zero
417        assert_eq!(cmd.rpc.auth_port, 0);
418        assert_eq!(cmd.rpc.http_port, 0);
419        assert_eq!(cmd.rpc.ws_port, 0);
420
421        // make sure the network ports are zero
422        assert_eq!(cmd.network.port, 0);
423        assert_eq!(cmd.network.discovery.port, 0);
424
425        // make sure the ipc path is not the default
426        assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
427    }
428}