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