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