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