1use 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#[derive(Debug, Parser)]
23pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs> {
24 #[arg(long, value_name = "FILE", verbatim_doc_comment)]
26 pub config: Option<PathBuf>,
27
28 #[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 #[command(flatten)]
44 pub metrics: MetricArgs,
45
46 #[arg(long, value_name = "INSTANCE", global = true, value_parser = value_parser!(u16).range(1..=200))]
61 pub instance: Option<u16>,
62
63 #[arg(long, conflicts_with = "instance", global = true)]
68 pub with_unused_ports: bool,
69
70 #[command(flatten)]
72 pub datadir: DatadirArgs,
73
74 #[command(flatten)]
76 pub network: NetworkArgs,
77
78 #[command(flatten)]
80 pub rpc: RpcServerArgs,
81
82 #[command(flatten)]
84 pub txpool: TxPoolArgs,
85
86 #[command(flatten)]
88 pub builder: PayloadBuilderArgs,
89
90 #[command(flatten)]
92 pub debug: DebugArgs,
93
94 #[command(flatten)]
96 pub db: DatabaseArgs,
97
98 #[command(flatten)]
100 pub dev: DevArgs,
101
102 #[command(flatten)]
104 pub pruning: PruningArgs,
105
106 #[command(flatten, next_help_heading = "Engine")]
108 pub engine: EngineArgs,
109
110 #[command(flatten, next_help_heading = "ERA")]
112 pub era: EraArgs,
113
114 #[command(flatten, next_help_heading = "Static Files")]
116 pub static_files: StaticFilesArgs,
117
118 #[command(flatten, next_help_heading = "Storage")]
120 pub storage: StorageArgs,
121
122 #[command(flatten, next_help_heading = "JIT")]
124 pub jit: JitArgs,
125
126 #[command(flatten, next_help_heading = "Extension")]
128 pub ext: Ext,
129}
130
131impl<C: ChainSpecParser> NodeCommand<C> {
132 pub fn parse_args() -> Self {
134 Self::parse()
135 }
136
137 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 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 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 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 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 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#[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 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 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 assert_eq!(cmd.rpc.auth_port, 8551);
397 assert_eq!(cmd.rpc.http_port, 8545);
398 assert_eq!(cmd.rpc.ws_port, 8546);
399 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 assert_eq!(cmd.rpc.auth_port, 8651);
408 assert_eq!(cmd.rpc.http_port, 8544);
409 assert_eq!(cmd.rpc.ws_port, 8548);
410 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 assert_eq!(cmd.rpc.auth_port, 8751);
419 assert_eq!(cmd.rpc.http_port, 8543);
420 assert_eq!(cmd.rpc.ws_port, 8550);
421 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 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 assert_eq!(cmd.network.port, 0);
457 assert_eq!(cmd.network.discovery.port, 0);
458
459 assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
461 }
462}