1use std::{
4 collections::HashSet,
5 ffi::OsStr,
6 net::{IpAddr, Ipv4Addr},
7 path::PathBuf,
8};
9
10use alloy_primitives::Address;
11use alloy_rpc_types_engine::JwtSecret;
12use clap::{
13 builder::{PossibleValue, RangedU64ValueParser, TypedValueParser},
14 Arg, Args, Command,
15};
16use rand::Rng;
17use reth_cli_util::parse_ether_value;
18use reth_rpc_eth_types::builder::config::PendingBlockKind;
19use reth_rpc_server_types::{constants, RethRpcModule, RpcModuleSelection};
20use url::Url;
21
22use crate::args::{
23 types::{MaxU32, ZeroAsNoneU64},
24 GasPriceOracleArgs, RpcStateCacheArgs,
25};
26
27use super::types::MaxOr;
28
29pub(crate) const RPC_DEFAULT_MAX_SUBS_PER_CONN: u32 = 1024;
31
32pub(crate) const RPC_DEFAULT_MAX_REQUEST_SIZE_MB: u32 = 15;
34
35pub(crate) const RPC_DEFAULT_MAX_RESPONSE_SIZE_MB: u32 = 160;
39
40pub(crate) const RPC_DEFAULT_MAX_CONNECTIONS: u32 = 500;
42
43#[derive(Debug, Clone, Args, PartialEq, Eq)]
45#[command(next_help_heading = "RPC")]
46pub struct RpcServerArgs {
47 #[arg(long, default_value_if("dev", "true", "true"))]
49 pub http: bool,
50
51 #[arg(long = "http.addr", default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
53 pub http_addr: IpAddr,
54
55 #[arg(long = "http.port", default_value_t = constants::DEFAULT_HTTP_RPC_PORT)]
57 pub http_port: u16,
58
59 #[arg(long = "http.disable-compression", default_value_t = false)]
61 pub http_disable_compression: bool,
62
63 #[arg(long = "http.api", value_parser = RpcModuleSelectionValueParser::default())]
65 pub http_api: Option<RpcModuleSelection>,
66
67 #[arg(long = "http.corsdomain")]
69 pub http_corsdomain: Option<String>,
70
71 #[arg(long)]
73 pub ws: bool,
74
75 #[arg(long = "ws.addr", default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
77 pub ws_addr: IpAddr,
78
79 #[arg(long = "ws.port", default_value_t = constants::DEFAULT_WS_RPC_PORT)]
81 pub ws_port: u16,
82
83 #[arg(id = "ws.origins", long = "ws.origins", alias = "ws.corsdomain")]
85 pub ws_allowed_origins: Option<String>,
86
87 #[arg(long = "ws.api", value_parser = RpcModuleSelectionValueParser::default())]
89 pub ws_api: Option<RpcModuleSelection>,
90
91 #[arg(long)]
93 pub ipcdisable: bool,
94
95 #[arg(long, default_value_t = constants::DEFAULT_IPC_ENDPOINT.to_string())]
97 pub ipcpath: String,
98
99 #[arg(long = "ipc.permissions")]
103 pub ipc_socket_permissions: Option<String>,
104
105 #[arg(long = "authrpc.addr", default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
107 pub auth_addr: IpAddr,
108
109 #[arg(long = "authrpc.port", default_value_t = constants::DEFAULT_AUTH_PORT)]
111 pub auth_port: u16,
112
113 #[arg(long = "authrpc.jwtsecret", value_name = "PATH", global = true, required = false)]
120 pub auth_jwtsecret: Option<PathBuf>,
121
122 #[arg(long)]
124 pub auth_ipc: bool,
125
126 #[arg(long = "auth-ipc.path", default_value_t = constants::DEFAULT_ENGINE_API_IPC_ENDPOINT.to_string())]
128 pub auth_ipc_path: String,
129
130 #[arg(long = "disable-auth-server", alias = "disable-engine-api")]
135 pub disable_auth_server: bool,
136
137 #[arg(long = "rpc.jwtsecret", value_name = "HEX", global = true, required = false)]
143 pub rpc_jwtsecret: Option<JwtSecret>,
144
145 #[arg(long = "rpc.max-request-size", alias = "rpc-max-request-size", default_value_t = RPC_DEFAULT_MAX_REQUEST_SIZE_MB.into())]
147 pub rpc_max_request_size: MaxU32,
148
149 #[arg(long = "rpc.max-response-size", alias = "rpc-max-response-size", visible_alias = "rpc.returndata.limit", default_value_t = RPC_DEFAULT_MAX_RESPONSE_SIZE_MB.into())]
151 pub rpc_max_response_size: MaxU32,
152
153 #[arg(long = "rpc.max-subscriptions-per-connection", alias = "rpc-max-subscriptions-per-connection", default_value_t = RPC_DEFAULT_MAX_SUBS_PER_CONN.into())]
155 pub rpc_max_subscriptions_per_connection: MaxU32,
156
157 #[arg(long = "rpc.max-connections", alias = "rpc-max-connections", value_name = "COUNT", default_value_t = RPC_DEFAULT_MAX_CONNECTIONS.into())]
159 pub rpc_max_connections: MaxU32,
160
161 #[arg(long = "rpc.max-tracing-requests", alias = "rpc-max-tracing-requests", value_name = "COUNT", default_value_t = constants::default_max_tracing_requests())]
168 pub rpc_max_tracing_requests: usize,
169
170 #[arg(long = "rpc.max-trace-filter-blocks", alias = "rpc-max-trace-filter-blocks", value_name = "COUNT", default_value_t = constants::DEFAULT_MAX_TRACE_FILTER_BLOCKS)]
172 pub rpc_max_trace_filter_blocks: u64,
173
174 #[arg(long = "rpc.max-blocks-per-filter", alias = "rpc-max-blocks-per-filter", value_name = "COUNT", default_value_t = ZeroAsNoneU64::new(constants::DEFAULT_MAX_BLOCKS_PER_FILTER))]
176 pub rpc_max_blocks_per_filter: ZeroAsNoneU64,
177
178 #[arg(long = "rpc.max-logs-per-response", alias = "rpc-max-logs-per-response", value_name = "COUNT", default_value_t = ZeroAsNoneU64::new(constants::DEFAULT_MAX_LOGS_PER_RESPONSE as u64))]
180 pub rpc_max_logs_per_response: ZeroAsNoneU64,
181
182 #[arg(
184 long = "rpc.gascap",
185 alias = "rpc-gascap",
186 value_name = "GAS_CAP",
187 value_parser = MaxOr::new(RangedU64ValueParser::<u64>::new().range(1..)),
188 default_value_t = constants::gas_oracle::RPC_DEFAULT_GAS_CAP
189 )]
190 pub rpc_gas_cap: u64,
191
192 #[arg(
194 long = "rpc.txfeecap",
195 alias = "rpc-txfeecap",
196 value_name = "TX_FEE_CAP",
197 value_parser = parse_ether_value,
198 default_value = "1.0"
199 )]
200 pub rpc_tx_fee_cap: u128,
201
202 #[arg(
204 long = "rpc.max-simulate-blocks",
205 value_name = "BLOCKS_COUNT",
206 default_value_t = constants::DEFAULT_MAX_SIMULATE_BLOCKS
207 )]
208 pub rpc_max_simulate_blocks: u64,
209
210 #[arg(
214 long = "rpc.eth-proof-window",
215 default_value_t = constants::DEFAULT_ETH_PROOF_WINDOW,
216 value_parser = RangedU64ValueParser::<u64>::new().range(..=constants::MAX_ETH_PROOF_WINDOW)
217 )]
218 pub rpc_eth_proof_window: u64,
219
220 #[arg(long = "rpc.proof-permits", alias = "rpc-proof-permits", value_name = "COUNT", default_value_t = constants::DEFAULT_PROOF_PERMITS)]
222 pub rpc_proof_permits: usize,
223
224 #[arg(long = "rpc.pending-block", default_value = "full", value_name = "KIND")]
229 pub rpc_pending_block: PendingBlockKind,
230
231 #[arg(long = "rpc.forwarder", alias = "rpc-forwarder", value_name = "FORWARDER")]
233 pub rpc_forwarder: Option<Url>,
234
235 #[arg(long = "builder.disallow", value_name = "PATH", value_parser = reth_cli_util::parsers::read_json_from_file::<HashSet<Address>>)]
238 pub builder_disallow: Option<HashSet<Address>>,
239
240 #[command(flatten)]
242 pub rpc_state_cache: RpcStateCacheArgs,
243
244 #[command(flatten)]
246 pub gas_price_oracle: GasPriceOracleArgs,
247}
248
249impl RpcServerArgs {
250 pub const fn with_http(mut self) -> Self {
252 self.http = true;
253 self
254 }
255
256 pub fn with_http_api(mut self, http_api: RpcModuleSelection) -> Self {
258 self.http_api = Some(http_api);
259 self
260 }
261
262 pub const fn with_ws(mut self) -> Self {
264 self.ws = true;
265 self
266 }
267
268 pub fn with_ws_api(mut self, ws_api: RpcModuleSelection) -> Self {
270 self.ws_api = Some(ws_api);
271 self
272 }
273
274 pub const fn with_auth_ipc(mut self) -> Self {
276 self.auth_ipc = true;
277 self
278 }
279
280 pub fn with_api(self, api: RpcModuleSelection) -> Self {
284 self.with_http_api(api.clone()).with_ws_api(api)
285 }
286
287 pub fn adjust_instance_ports(&mut self, instance: Option<u16>) {
302 if let Some(instance) = instance {
303 debug_assert_ne!(instance, 0, "instance must be non-zero");
304 self.auth_port += instance * 100 - 100;
306 self.http_port -= instance - 1;
308 self.ws_port += instance * 2 - 2;
310 self.ipcpath = format!("{}-{}", self.ipcpath, instance);
312 }
313 }
314
315 pub const fn with_http_unused_port(mut self) -> Self {
318 self.http_port = 0;
319 self
320 }
321
322 pub const fn with_ws_unused_port(mut self) -> Self {
325 self.ws_port = 0;
326 self
327 }
328
329 pub const fn with_auth_unused_port(mut self) -> Self {
332 self.auth_port = 0;
333 self
334 }
335
336 pub fn with_ipc_random_path(mut self) -> Self {
339 let random_string: String =
340 rand::rng().sample_iter(rand::distr::Alphanumeric).take(8).map(char::from).collect();
341 self.ipcpath = format!("{}-{}", self.ipcpath, random_string);
342 self
343 }
344
345 pub fn with_unused_ports(mut self) -> Self {
348 self = self.with_http_unused_port();
349 self = self.with_ws_unused_port();
350 self = self.with_auth_unused_port();
351 self = self.with_ipc_random_path();
352 self
353 }
354
355 pub fn apply<F>(self, f: F) -> Self
357 where
358 F: FnOnce(Self) -> Self,
359 {
360 f(self)
361 }
362}
363
364impl Default for RpcServerArgs {
365 fn default() -> Self {
366 Self {
367 http: false,
368 http_addr: Ipv4Addr::LOCALHOST.into(),
369 http_port: constants::DEFAULT_HTTP_RPC_PORT,
370 http_disable_compression: false,
371 http_api: None,
372 http_corsdomain: None,
373 ws: false,
374 ws_addr: Ipv4Addr::LOCALHOST.into(),
375 ws_port: constants::DEFAULT_WS_RPC_PORT,
376 ws_allowed_origins: None,
377 ws_api: None,
378 ipcdisable: false,
379 ipcpath: constants::DEFAULT_IPC_ENDPOINT.to_string(),
380 ipc_socket_permissions: None,
381 auth_addr: Ipv4Addr::LOCALHOST.into(),
382 auth_port: constants::DEFAULT_AUTH_PORT,
383 auth_jwtsecret: None,
384 auth_ipc: false,
385 auth_ipc_path: constants::DEFAULT_ENGINE_API_IPC_ENDPOINT.to_string(),
386 disable_auth_server: false,
387 rpc_jwtsecret: None,
388 rpc_max_request_size: RPC_DEFAULT_MAX_REQUEST_SIZE_MB.into(),
389 rpc_max_response_size: RPC_DEFAULT_MAX_RESPONSE_SIZE_MB.into(),
390 rpc_max_subscriptions_per_connection: RPC_DEFAULT_MAX_SUBS_PER_CONN.into(),
391 rpc_max_connections: RPC_DEFAULT_MAX_CONNECTIONS.into(),
392 rpc_max_tracing_requests: constants::default_max_tracing_requests(),
393 rpc_max_trace_filter_blocks: constants::DEFAULT_MAX_TRACE_FILTER_BLOCKS,
394 rpc_max_blocks_per_filter: constants::DEFAULT_MAX_BLOCKS_PER_FILTER.into(),
395 rpc_max_logs_per_response: (constants::DEFAULT_MAX_LOGS_PER_RESPONSE as u64).into(),
396 rpc_gas_cap: constants::gas_oracle::RPC_DEFAULT_GAS_CAP,
397 rpc_tx_fee_cap: constants::DEFAULT_TX_FEE_CAP_WEI,
398 rpc_max_simulate_blocks: constants::DEFAULT_MAX_SIMULATE_BLOCKS,
399 rpc_eth_proof_window: constants::DEFAULT_ETH_PROOF_WINDOW,
400 rpc_pending_block: PendingBlockKind::Full,
401 gas_price_oracle: GasPriceOracleArgs::default(),
402 rpc_state_cache: RpcStateCacheArgs::default(),
403 rpc_proof_permits: constants::DEFAULT_PROOF_PERMITS,
404 rpc_forwarder: None,
405 builder_disallow: Default::default(),
406 }
407 }
408}
409
410#[derive(Clone, Debug, Default)]
412#[non_exhaustive]
413struct RpcModuleSelectionValueParser;
414
415impl TypedValueParser for RpcModuleSelectionValueParser {
416 type Value = RpcModuleSelection;
417
418 fn parse_ref(
419 &self,
420 _cmd: &Command,
421 arg: Option<&Arg>,
422 value: &OsStr,
423 ) -> Result<Self::Value, clap::Error> {
424 let val =
425 value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
426 val.parse::<RpcModuleSelection>().map_err(|err| {
427 let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned());
428 let possible_values = RethRpcModule::all_variant_names().to_vec().join(",");
429 let msg = format!(
430 "Invalid value '{val}' for {arg}: {err}.\n [possible values: {possible_values}]"
431 );
432 clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg)
433 })
434 }
435
436 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
437 let values = RethRpcModule::all_variant_names().iter().map(PossibleValue::new);
438 Some(Box::new(values))
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use clap::{Args, Parser};
446
447 #[derive(Parser)]
449 struct CommandParser<T: Args> {
450 #[command(flatten)]
451 args: T,
452 }
453
454 #[test]
455 fn test_rpc_server_args_parser() {
456 let args =
457 CommandParser::<RpcServerArgs>::parse_from(["reth", "--http.api", "eth,admin,debug"])
458 .args;
459
460 let apis = args.http_api.unwrap();
461 let expected = RpcModuleSelection::try_from_selection(["eth", "admin", "debug"]).unwrap();
462
463 assert_eq!(apis, expected);
464 }
465
466 #[test]
467 fn test_rpc_server_eth_call_bundle_args() {
468 let args =
469 CommandParser::<RpcServerArgs>::parse_from(["reth", "--http.api", "eth,admin,debug"])
470 .args;
471
472 let apis = args.http_api.unwrap();
473 let expected = RpcModuleSelection::try_from_selection(["eth", "admin", "debug"]).unwrap();
474
475 assert_eq!(apis, expected);
476 }
477
478 #[test]
479 fn test_rpc_server_args_parser_none() {
480 let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--http.api", "none"]).args;
481 let apis = args.http_api.unwrap();
482 let expected = RpcModuleSelection::Selection(Default::default());
483 assert_eq!(apis, expected);
484 }
485
486 #[test]
487 fn rpc_server_args_default_sanity_test() {
488 let default_args = RpcServerArgs::default();
489 let args = CommandParser::<RpcServerArgs>::parse_from(["reth"]).args;
490
491 assert_eq!(args, default_args);
492 }
493
494 #[test]
495 fn test_rpc_tx_fee_cap_parse_integer() {
496 let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "2"]).args;
497 let expected = 2_000_000_000_000_000_000u128; assert_eq!(args.rpc_tx_fee_cap, expected);
499 }
500
501 #[test]
502 fn test_rpc_tx_fee_cap_parse_decimal() {
503 let args =
504 CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "1.5"]).args;
505 let expected = 1_500_000_000_000_000_000u128; assert_eq!(args.rpc_tx_fee_cap, expected);
507 }
508
509 #[test]
510 fn test_rpc_tx_fee_cap_parse_zero() {
511 let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "0"]).args;
512 assert_eq!(args.rpc_tx_fee_cap, 0); }
514
515 #[test]
516 fn test_rpc_tx_fee_cap_parse_none() {
517 let args = CommandParser::<RpcServerArgs>::parse_from(["reth"]).args;
518 let expected = 1_000_000_000_000_000_000u128;
519 assert_eq!(args.rpc_tx_fee_cap, expected); }
521}