1use clap::{value_parser, Args, Parser};
4use reth_chainspec::{EthChainSpec, EthereumHardforks};
5use reth_cli::chainspec::ChainSpecParser;
6use reth_cli_runner::CliContext;
7use reth_cli_util::parse_socket_address;
8use reth_db::{init_db, DatabaseEnv};
9use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
10use reth_node_builder::{NodeBuilder, WithLaunchContext};
11use reth_node_core::{
12 args::{
13 DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EnclaveArgs, NetworkArgs,
14 PayloadBuilderArgs, PruningArgs, RpcServerArgs, TxPoolArgs,
15 },
16 node_config::NodeConfig,
17 version,
18};
19use std::{ffi::OsString, fmt, future::Future, net::SocketAddr, path::PathBuf, sync::Arc};
20
21#[derive(Debug, Parser)]
23pub struct NodeCommand<
24 C: ChainSpecParser = EthereumChainSpecParser,
25 Ext: clap::Args + fmt::Debug = NoArgs,
26> {
27 #[arg(long, value_name = "FILE", verbatim_doc_comment)]
29 pub config: Option<PathBuf>,
30
31 #[arg(
35 long,
36 value_name = "CHAIN_OR_PATH",
37 long_help = C::help_message(),
38 default_value = C::SUPPORTED_CHAINS[0],
39 default_value_if("dev", "true", "dev"),
40 value_parser = C::parser(),
41 required = false,
42 )]
43 pub chain: Arc<C::ChainSpec>,
44
45 #[arg(long, value_name = "SOCKET", value_parser = parse_socket_address, help_heading = "Metrics")]
49 pub metrics: Option<SocketAddr>,
50
51 #[arg(long, value_name = "INSTANCE", global = true, default_value_t = 1, value_parser = value_parser!(u16).range(..=200))]
65 pub instance: u16,
66
67 #[arg(long, conflicts_with = "instance", global = true)]
72 pub with_unused_ports: bool,
73
74 #[command(flatten)]
76 pub datadir: DatadirArgs,
77
78 #[command(flatten)]
80 pub network: NetworkArgs,
81
82 #[command(flatten)]
84 pub rpc: RpcServerArgs,
85
86 #[command(flatten)]
88 pub txpool: TxPoolArgs,
89
90 #[command(flatten)]
92 pub builder: PayloadBuilderArgs,
93
94 #[command(flatten)]
96 pub debug: DebugArgs,
97
98 #[command(flatten)]
100 pub db: DatabaseArgs,
101
102 #[command(flatten)]
104 pub dev: DevArgs,
105
106 #[command(flatten)]
108 pub pruning: PruningArgs,
109
110 #[command(flatten, next_help_heading = "Extension")]
112 pub ext: Ext,
113
114 #[command(flatten)]
116 pub enclave: EnclaveArgs,
117}
118
119impl<C: ChainSpecParser> NodeCommand<C> {
120 pub fn parse_args() -> Self {
122 Self::parse()
123 }
124
125 pub fn try_parse_args_from<I, T>(itr: I) -> Result<Self, clap::error::Error>
127 where
128 I: IntoIterator<Item = T>,
129 T: Into<OsString> + Clone,
130 {
131 Self::try_parse_from(itr)
132 }
133}
134
135impl<
136 C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>,
137 Ext: clap::Args + fmt::Debug,
138 > NodeCommand<C, Ext>
139{
140 pub async fn execute<L, Fut>(self, ctx: CliContext, launcher: L) -> eyre::Result<()>
145 where
146 L: FnOnce(WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, C::ChainSpec>>, Ext) -> Fut,
147 Fut: Future<Output = eyre::Result<()>>,
148 {
149 tracing::info!(target: "reth::cli", version = ?version::SHORT_VERSION, "Starting reth");
150
151 let Self {
152 datadir,
153 config,
154 chain,
155 metrics,
156 instance,
157 with_unused_ports,
158 network,
159 rpc,
160 txpool,
161 builder,
162 debug,
163 db,
164 dev,
165 pruning,
166 ext,
167 enclave,
168 } = self;
169
170 let mut node_config = NodeConfig {
172 datadir,
173 config,
174 chain,
175 metrics,
176 instance,
177 network,
178 rpc,
179 txpool,
180 builder,
181 debug,
182 db,
183 dev,
184 pruning,
185 enclave,
186 };
187
188 let data_dir = node_config.datadir();
189 let db_path = data_dir.db();
190
191 tracing::info!(target: "reth::cli", path = ?db_path, "Opening database");
192 let database = Arc::new(init_db(db_path.clone(), self.db.database_args())?.with_metrics());
193
194 if with_unused_ports {
195 node_config = node_config.with_unused_ports();
196 }
197
198 let builder = NodeBuilder::new(node_config)
199 .with_database(database)
200 .with_launch_context(ctx.task_executor);
201
202 launcher(builder, ext).await
203 }
204}
205
206#[derive(Debug, Clone, Copy, Default, Args)]
208#[non_exhaustive]
209pub struct NoArgs;
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use reth_discv4::DEFAULT_DISCOVERY_PORT;
215 use reth_ethereum_cli::chainspec::SUPPORTED_CHAINS;
216 use std::{
217 net::{IpAddr, Ipv4Addr},
218 path::Path,
219 };
220
221 #[test]
222 fn parse_help_node_command() {
223 let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from(["reth", "--help"])
224 .unwrap_err();
225 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
226 }
227
228 #[test]
229 fn parse_common_node_command_chain_args() {
230 for chain in SUPPORTED_CHAINS {
231 let args: NodeCommand = NodeCommand::parse_from(["reth", "--chain", chain]);
232 assert_eq!(args.chain.chain, chain.parse::<reth_chainspec::Chain>().unwrap());
233 }
234 }
235
236 #[test]
237 fn parse_discovery_addr() {
238 let cmd: NodeCommand =
239 NodeCommand::try_parse_args_from(["reth", "--discovery.addr", "127.0.0.1"]).unwrap();
240 assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
241 }
242
243 #[test]
244 fn parse_addr() {
245 let cmd: NodeCommand = NodeCommand::try_parse_args_from([
246 "reth",
247 "--discovery.addr",
248 "127.0.0.1",
249 "--addr",
250 "127.0.0.1",
251 ])
252 .unwrap();
253 assert_eq!(cmd.network.discovery.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
254 assert_eq!(cmd.network.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
255 }
256
257 #[test]
258 fn parse_discovery_port() {
259 let cmd: NodeCommand =
260 NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300"]).unwrap();
261 assert_eq!(cmd.network.discovery.port, 300);
262 }
263
264 #[test]
265 fn parse_port() {
266 let cmd: NodeCommand =
267 NodeCommand::try_parse_args_from(["reth", "--discovery.port", "300", "--port", "99"])
268 .unwrap();
269 assert_eq!(cmd.network.discovery.port, 300);
270 assert_eq!(cmd.network.port, 99);
271 }
272
273 #[test]
274 fn parse_metrics_port() {
275 let cmd: NodeCommand =
276 NodeCommand::try_parse_args_from(["reth", "--metrics", "9001"]).unwrap();
277 assert_eq!(cmd.metrics, Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001)));
278
279 let cmd: NodeCommand =
280 NodeCommand::try_parse_args_from(["reth", "--metrics", ":9001"]).unwrap();
281 assert_eq!(cmd.metrics, Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001)));
282
283 let cmd: NodeCommand =
284 NodeCommand::try_parse_args_from(["reth", "--metrics", "localhost:9001"]).unwrap();
285 assert_eq!(cmd.metrics, Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001)));
286 }
287
288 #[test]
289 fn parse_config_path() {
290 let cmd: NodeCommand =
291 NodeCommand::try_parse_args_from(["reth", "--config", "my/path/to/reth.toml"]).unwrap();
292 let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
294 let config_path = cmd.config.unwrap_or_else(|| data_dir.config());
295 assert_eq!(config_path, Path::new("my/path/to/reth.toml"));
296
297 let cmd: NodeCommand = NodeCommand::try_parse_args_from(["reth"]).unwrap();
298
299 let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
301 let config_path = cmd.config.clone().unwrap_or_else(|| data_dir.config());
302 let end = format!("{}/reth.toml", SUPPORTED_CHAINS[0]);
303 assert!(config_path.ends_with(end), "{:?}", cmd.config);
304 }
305
306 #[test]
307 fn parse_db_path() {
308 let cmd: NodeCommand = NodeCommand::try_parse_args_from(["reth"]).unwrap();
309 let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
310
311 let db_path = data_dir.db();
312 let end = format!("reth/{}/db", SUPPORTED_CHAINS[0]);
313 assert!(db_path.ends_with(end), "{:?}", cmd.config);
314
315 let cmd: NodeCommand =
316 NodeCommand::try_parse_args_from(["reth", "--datadir", "my/custom/path"]).unwrap();
317 let data_dir = cmd.datadir.resolve_datadir(cmd.chain.chain);
318
319 let db_path = data_dir.db();
320 assert_eq!(db_path, Path::new("my/custom/path/db"));
321 }
322
323 #[test]
324 fn parse_instance() {
325 let mut cmd: NodeCommand = NodeCommand::parse_from(["reth"]);
326 cmd.rpc.adjust_instance_ports(cmd.instance);
327 cmd.network.port = DEFAULT_DISCOVERY_PORT + cmd.instance - 1;
328 assert_eq!(cmd.rpc.auth_port, 8551);
330 assert_eq!(cmd.rpc.http_port, 8545);
331 assert_eq!(cmd.rpc.ws_port, 8546);
332 assert_eq!(cmd.network.port, 30303);
334
335 let mut cmd: NodeCommand = NodeCommand::parse_from(["reth", "--instance", "2"]);
336 cmd.rpc.adjust_instance_ports(cmd.instance);
337 cmd.network.port = DEFAULT_DISCOVERY_PORT + cmd.instance - 1;
338 assert_eq!(cmd.rpc.auth_port, 8651);
340 assert_eq!(cmd.rpc.http_port, 8544);
341 assert_eq!(cmd.rpc.ws_port, 8548);
342 assert_eq!(cmd.network.port, 30304);
344
345 let mut cmd: NodeCommand = NodeCommand::parse_from(["reth", "--instance", "3"]);
346 cmd.rpc.adjust_instance_ports(cmd.instance);
347 cmd.network.port = DEFAULT_DISCOVERY_PORT + cmd.instance - 1;
348 assert_eq!(cmd.rpc.auth_port, 8751);
350 assert_eq!(cmd.rpc.http_port, 8543);
351 assert_eq!(cmd.rpc.ws_port, 8550);
352 assert_eq!(cmd.network.port, 30305);
354 }
355
356 #[test]
357 fn parse_with_unused_ports() {
358 let cmd: NodeCommand = NodeCommand::parse_from(["reth", "--with-unused-ports"]);
359 assert!(cmd.with_unused_ports);
360 }
361
362 #[test]
363 fn with_unused_ports_conflicts_with_instance() {
364 let err = NodeCommand::<EthereumChainSpecParser>::try_parse_args_from([
365 "reth",
366 "--with-unused-ports",
367 "--instance",
368 "2",
369 ])
370 .unwrap_err();
371 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
372 }
373
374 #[test]
375 fn with_unused_ports_check_zero() {
376 let mut cmd: NodeCommand = NodeCommand::parse_from(["reth"]);
377 cmd.rpc = cmd.rpc.with_unused_ports();
378 cmd.network = cmd.network.with_unused_ports();
379
380 assert_eq!(cmd.rpc.auth_port, 0);
382 assert_eq!(cmd.rpc.http_port, 0);
383 assert_eq!(cmd.rpc.ws_port, 0);
384
385 assert_eq!(cmd.network.port, 0);
387 assert_eq!(cmd.network.discovery.port, 0);
388
389 assert_ne!(cmd.rpc.ipcpath, String::from("/tmp/reth.ipc"));
391 }
392}