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))]
62 pub instance: Option<u16>,
63
64 #[arg(long, conflicts_with = "instance", global = true)]
69 pub with_unused_ports: bool,
70
71 #[command(flatten)]
73 pub datadir: DatadirArgs,
74
75 #[command(flatten)]
77 pub network: NetworkArgs,
78
79 #[command(flatten)]
81 pub rpc: RpcServerArgs,
82
83 #[command(flatten)]
85 pub txpool: TxPoolArgs,
86
87 #[command(flatten)]
89 pub builder: PayloadBuilderArgs,
90
91 #[command(flatten)]
93 pub debug: DebugArgs,
94
95 #[command(flatten)]
97 pub db: DatabaseArgs,
98
99 #[command(flatten)]
101 pub dev: DevArgs,
102
103 #[command(flatten)]
105 pub pruning: PruningArgs,
106
107 #[command(flatten, next_help_heading = "Engine")]
109 pub engine: EngineArgs,
110
111 #[command(flatten, next_help_heading = "ERA")]
113 pub era: EraArgs,
114
115 #[command(flatten, next_help_heading = "Static Files")]
117 pub static_files: StaticFilesArgs,
118
119 #[command(flatten, next_help_heading = "Storage")]
121 pub storage: StorageArgs,
122
123 #[command(flatten, next_help_heading = "JIT")]
125 pub jit: JitArgs,
126
127 #[command(flatten, next_help_heading = "Extension")]
129 pub ext: Ext,
130}
131
132impl<C: ChainSpecParser> NodeCommand<C> {
133 pub fn parse_args() -> Self {
135 Self::parse()
136 }
137
138 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 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 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 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 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 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#[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 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 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 assert_eq!(cmd.rpc.auth_port, 8551);
398 assert_eq!(cmd.rpc.http_port, 8545);
399 assert_eq!(cmd.rpc.ws_port, 8546);
400 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 assert_eq!(cmd.rpc.auth_port, 8651);
409 assert_eq!(cmd.rpc.http_port, 8544);
410 assert_eq!(cmd.rpc.ws_port, 8548);
411 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 assert_eq!(cmd.rpc.auth_port, 8751);
420 assert_eq!(cmd.rpc.http_port, 8543);
421 assert_eq!(cmd.rpc.ws_port, 8550);
422 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 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 assert_eq!(cmd.network.port, 0);
458 assert_eq!(cmd.network.discovery.port, 0);
459
460 assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
462 }
463}