1use crate::{
19 DbContext, DebugRpcServer, DebugRpcServerImpl, EthRpcServer, EthRpcServerImpl, LOG_TARGET,
20 PolkadotRpcServer, PolkadotRpcServerImpl, ReceiptExtractor, ReceiptProvider,
21 SubxtBlockInfoProvider, SystemHealthRpcServer, SystemHealthRpcServerImpl,
22 client::{
23 Client, ClientError, SubscriptionGapQueue, SubscriptionType, connect,
24 version_aware_runtime_api::VersionAwareRuntimeApiProvider,
25 },
26};
27use clap::{CommandFactory, FromArgMatches, Parser};
28use futures::{FutureExt, future::BoxFuture, pin_mut};
29use jsonrpsee::server::RpcModule;
30use sc_cli::{PrometheusParams, RpcParams, SharedParams, Signals};
31use sc_service::{
32 TaskManager,
33 config::{BasePath, PrometheusConfig, RpcConfiguration},
34 create_rpc_runtime, start_rpc_servers,
35};
36use sqlx::{
37 SqlitePool,
38 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
39};
40use std::path::PathBuf;
41
42async fn sqlite_db_query_max_variable_number(pool: &SqlitePool) -> usize {
44 let limit = async {
45 let mut conn = pool
46 .acquire()
47 .await
48 .inspect_err(|e| log::warn!(target: LOG_TARGET, "๐พ Failed to acquire connection: {e}"))
49 .ok()?;
50 let mut handle = conn
51 .lock_handle()
52 .await
53 .inspect_err(|e| log::warn!(target: LOG_TARGET, "๐พ Failed to lock handle: {e}"))
54 .ok()?;
55 let raw = unsafe {
58 libsqlite3_sys::sqlite3_limit(
59 handle.as_raw_handle().as_ptr(),
60 libsqlite3_sys::SQLITE_LIMIT_VARIABLE_NUMBER,
61 -1,
62 )
63 };
64 raw.try_into().ok()
65 }
66 .await;
67
68 let default = DbContext::DEFAULT_MAX_VARIABLE_NUMBER;
69 limit.inspect(|n| log::info!(target: LOG_TARGET, "๐พ SQLite db_query_max_variable_number: {n}"))
70 .unwrap_or_else(|| {
71 log::warn!(target: LOG_TARGET, "๐พ Failed to query SQLite variable limit, falling back to {default}");
72 default
73 })
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)]
78pub enum EthPruningMode {
79 #[display(fmt = "archive")]
81 Archive,
82 #[display(fmt = "{_0}")]
84 KeepLatest(usize),
85}
86
87impl EthPruningMode {
88 pub fn is_archive(&self) -> bool {
90 matches!(self, Self::Archive)
91 }
92
93 pub fn keep_latest(&self) -> Option<usize> {
95 match self {
96 Self::KeepLatest(n) => Some(*n),
97 _ => None,
98 }
99 }
100}
101
102impl std::str::FromStr for EthPruningMode {
103 type Err = String;
104
105 fn from_str(input: &str) -> Result<Self, Self::Err> {
106 match input {
107 "archive" => Ok(Self::Archive),
108 n => {
109 n.parse::<usize>()
110 .ok()
111 .filter(|&v| v >= 1)
112 .map(Self::KeepLatest)
113 .ok_or_else(|| {
114 format!(
115 "Invalid pruning mode '{n}': expected 'archive' or a positive integer"
116 )
117 })
118 },
119 }
120 }
121}
122
123const DEFAULT_PROMETHEUS_PORT: u16 = 9616;
125
126const DEFAULT_RPC_PORT: u16 = 8545;
128
129const DEFAULT_DATABASE_NAME: &str = "eth-rpc.db";
130
131#[derive(Parser, Debug)]
133#[clap(author, about, version)]
134pub struct CliCommand {
135 #[clap(long, default_value = "ws://127.0.0.1:9944")]
137 pub node_rpc_url: String,
138
139 #[clap(long, default_value = "archive")]
144 pub eth_pruning: EthPruningMode,
145
146 #[allow(missing_docs)]
147 #[clap(flatten)]
148 pub shared_params: SharedParams,
149
150 #[allow(missing_docs)]
151 #[clap(flatten)]
152 pub rpc_params: RpcParams,
153
154 #[allow(missing_docs)]
155 #[clap(flatten)]
156 pub prometheus_params: PrometheusParams,
157
158 #[arg(long)]
162 pub allow_unprotected_txs: bool,
163}
164
165impl CliCommand {
166 pub fn parse_cli() -> anyhow::Result<Self> {
168 let removed_flags =
169 ["database-url", "cache-size", "index-last-n-blocks", "earliest-receipt-block"];
170
171 let cmd = removed_flags.iter().fold(Self::command(), |cmd, name| {
172 cmd.arg(
173 clap::Arg::new(*name)
174 .long(*name)
175 .num_args(0..=1)
176 .hide(true)
177 .action(clap::ArgAction::Set),
178 )
179 });
180 let matches = cmd.get_matches();
181
182 let used: Vec<_> = removed_flags
183 .iter()
184 .filter(|f| matches.contains_id(f))
185 .map(|f| format!("--{f}"))
186 .collect();
187 if !used.is_empty() {
188 anyhow::bail!(
189 "[{}] have been removed. \
190 Check polkadot-sdk PR #11153 for the CLI migration guide.",
191 used.join(", "),
192 );
193 }
194
195 Ok(Self::from_arg_matches(&matches).expect("already validated by clap"))
196 }
197}
198
199#[cfg(not(test))]
201fn init_logger(params: &SharedParams) -> anyhow::Result<()> {
202 let mut logger = sc_cli::LoggerBuilder::new(params.log_filters().join(","));
203 logger
204 .with_log_reloading(params.enable_log_reloading)
205 .with_detailed_output(params.detailed_log_output);
206
207 if let Some(tracing_targets) = ¶ms.tracing_targets {
208 let tracing_receiver = params.tracing_receiver.into();
209 logger.with_profiling(tracing_receiver, tracing_targets);
210 }
211
212 if params.disable_log_color {
213 logger.with_colors(false);
214 }
215
216 logger.init()?;
217 Ok(())
218}
219
220fn resolve_db_dir(base_path: Option<BasePath>) -> PathBuf {
228 match base_path {
229 Some(path) => path.path().to_path_buf(),
230 None => BasePath::from_project("", "", "eth-rpc").path().to_path_buf(),
231 }
232}
233
234fn resolve_db_options(
236 eth_pruning: EthPruningMode,
237 base_path: Option<BasePath>,
238) -> anyhow::Result<SqliteConnectOptions> {
239 if eth_pruning.is_archive() {
240 let db_dir = resolve_db_dir(base_path);
241 std::fs::create_dir_all(&db_dir).map_err(|e| {
242 anyhow::anyhow!("Failed to create database directory {}: {e}", db_dir.display())
243 })?;
244 let db_path = db_dir.join(DEFAULT_DATABASE_NAME);
245 log::info!(target: LOG_TARGET, "๐พ Database path: {}", db_path.display());
246 Ok(SqliteConnectOptions::new()
249 .filename(&db_path)
250 .create_if_missing(true)
251 .journal_mode(SqliteJournalMode::Wal))
252 } else {
253 Ok(SqliteConnectOptions::new().in_memory(true))
254 }
255}
256
257fn build_client(
258 tokio_handle: &tokio::runtime::Handle,
259 eth_pruning: EthPruningMode,
260 node_rpc_url: &str,
261 db_options: SqliteConnectOptions,
262 max_request_size: u32,
263 max_response_size: u32,
264 abort_signal: Signals,
265 subscription_gap_queue: SubscriptionGapQueue,
266) -> anyhow::Result<Client> {
267 let fut = async {
268 let (api, rpc_client, rpc) =
269 connect(node_rpc_url, max_request_size, max_response_size).await?;
270 let block_provider = SubxtBlockInfoProvider::new(api.clone(), rpc.clone()).await?;
271
272 let (pool, keep_latest_n_blocks) = match eth_pruning {
273 EthPruningMode::Archive => {
274 (SqlitePoolOptions::new().connect_with(db_options).await?, None)
275 },
276 EthPruningMode::KeepLatest(max_blocks) => {
277 log::info!(target: LOG_TARGET,
278 "๐พ Using in-memory database, keeping only {max_blocks} blocks");
279 let pool = SqlitePoolOptions::new()
281 .max_connections(1)
282 .idle_timeout(None)
283 .max_lifetime(None)
284 .connect_with(db_options)
285 .await?;
286 (pool, Some(max_blocks))
287 },
288 };
289
290 let runtime_api_provider =
291 VersionAwareRuntimeApiProvider::new(api.clone(), rpc_client.clone());
292 let receipt_extractor = ReceiptExtractor::new(runtime_api_provider.clone()).await?;
293 let max_variable_number = sqlite_db_query_max_variable_number(&pool).await;
294 let db_ctx = DbContext::new(pool, max_variable_number);
295
296 let receipt_provider = ReceiptProvider::new(
297 db_ctx,
298 block_provider.clone(),
299 receipt_extractor.clone(),
300 keep_latest_n_blocks,
301 )
302 .await?;
303
304 let client = Client::new(
305 api,
306 rpc_client,
307 rpc,
308 block_provider,
309 receipt_provider,
310 eth_pruning.is_archive(),
311 subscription_gap_queue,
312 runtime_api_provider,
313 )
314 .await?;
315
316 Ok(client)
317 }
318 .fuse();
319 pin_mut!(fut);
320
321 match tokio_handle.block_on(abort_signal.try_until_signal(fut)) {
322 Ok(Ok(client)) => Ok(client),
323 Ok(Err(err)) => Err(err),
324 Err(_) => anyhow::bail!("Process interrupted"),
325 }
326}
327
328pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
330 let CliCommand {
331 rpc_params,
332 prometheus_params,
333 node_rpc_url,
334 eth_pruning,
335 shared_params,
336 allow_unprotected_txs,
337 ..
338 } = cmd;
339
340 #[cfg(not(test))]
341 init_logger(&shared_params)?;
342 let is_dev = shared_params.dev;
343 let explicit_base_path = shared_params.base_path.is_some();
344 let base_path = shared_params.base_path()?;
345
346 if is_dev && eth_pruning.is_archive() && !explicit_base_path {
347 log::warn!(
348 target: LOG_TARGET,
349 "โ ๏ธ Running in --dev mode with --eth-pruning=archive but no --base-path. \
350 The database will be stored in a temporary directory and lost on exit. \
351 Use --base-path to persist the database."
352 );
353 }
354
355 let db_options = resolve_db_options(eth_pruning, base_path)?;
356
357 let rpc_addrs: Option<Vec<sc_service::config::RpcEndpoint>> = rpc_params
358 .rpc_addr(is_dev, false, DEFAULT_RPC_PORT)?
359 .map(|addrs| addrs.into_iter().map(Into::into).collect());
360
361 let rpc_config = RpcConfiguration {
362 addr: rpc_addrs,
363 methods: rpc_params.rpc_methods.into(),
364 max_connections: rpc_params.rpc_max_connections,
365 cors: rpc_params.rpc_cors(is_dev)?,
366 max_request_size: rpc_params.rpc_max_request_size,
367 max_response_size: rpc_params.rpc_max_response_size,
368 id_provider: None,
369 max_subs_per_conn: rpc_params.rpc_max_subscriptions_per_connection,
370 port: rpc_params.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
371 message_buffer_capacity: rpc_params.rpc_message_buffer_capacity_per_connection,
372 batch_config: rpc_params.rpc_batch_config()?,
373 rate_limit: rpc_params.rpc_rate_limit,
374 rate_limit_whitelisted_ips: rpc_params.rpc_rate_limit_whitelisted_ips,
375 rate_limit_trust_proxy_headers: rpc_params.rpc_rate_limit_trust_proxy_headers,
376 request_logger_limit: if is_dev { 1024 * 1024 } else { 1024 },
377 };
378
379 let prometheus_config =
380 prometheus_params.prometheus_config(DEFAULT_PROMETHEUS_PORT, "eth-rpc".into());
381 let prometheus_registry = prometheus_config.as_ref().map(|config| &config.registry);
382
383 let tokio_runtime = sc_cli::build_runtime()?;
384 let tokio_handle = tokio_runtime.handle();
385 let mut task_manager = TaskManager::new(tokio_handle.clone(), prometheus_registry)?;
386
387 let (subscription_gap_queue, gap_fill_rx) = SubscriptionGapQueue::new();
388 let client = build_client(
389 tokio_handle,
390 eth_pruning,
391 &node_rpc_url,
392 db_options,
393 rpc_config.max_request_size * 1024 * 1024,
394 rpc_config.max_response_size * 1024 * 1024,
395 tokio_runtime.block_on(async { Signals::capture() })?,
396 subscription_gap_queue,
397 )?;
398
399 if let Some(PrometheusConfig { port, registry }) = prometheus_config.clone() {
401 task_manager.spawn_handle().spawn(
402 "prometheus-endpoint",
403 None,
404 prometheus_endpoint::init_prometheus(port, registry).map(drop),
405 );
406 }
407
408 let rpc_runtime = create_rpc_runtime(rpc_config.max_connections)
409 .map_err(|e| anyhow::anyhow!("Failed to create RPC runtime: {}", e))?;
410
411 let rpc_api = rpc_module(is_dev, client.clone(), allow_unprotected_txs)?;
412 let rpc_server_handle = start_rpc_servers(
413 &rpc_config,
414 prometheus_registry,
415 tokio_handle,
416 rpc_api,
417 rpc_runtime,
418 None,
419 )?;
420
421 task_manager
422 .spawn_essential_handle()
423 .spawn("block-subscription", None, async move {
424 let mut futures: Vec<BoxFuture<'_, Result<(), _>>> = vec![
425 Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::BestBlocks)),
426 Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::FinalizedBlocks)),
427 ];
428
429 if eth_pruning.is_archive() {
430 futures.push(Box::pin(client.sync_backward()));
431 }
432
433 futures.push(Box::pin(async {
435 client.run_subscription_gap_filler(gap_fill_rx).await;
436 Ok::<_, ClientError>(())
437 }));
438
439 if let Err(err) = futures::future::try_join_all(futures).await {
440 panic!("Block subscription task failed: {err:?}",)
441 }
442 });
443
444 task_manager.keep_alive(rpc_server_handle);
445 let signals = tokio_runtime.block_on(async { Signals::capture() })?;
446 tokio_runtime.block_on(signals.run_until_signal(task_manager.future().fuse()))?;
447 Ok(())
448}
449
450fn rpc_module(
452 is_dev: bool,
453 client: Client,
454 allow_unprotected_txs: bool,
455) -> Result<RpcModule<()>, sc_service::Error> {
456 let eth_api = EthRpcServerImpl::new(client.clone())
457 .with_accounts(if is_dev {
458 vec![
459 crate::Account::from(subxt_signer::eth::dev::alith()),
460 crate::Account::from(subxt_signer::eth::dev::baltathar()),
461 crate::Account::from(subxt_signer::eth::dev::charleth()),
462 crate::Account::from(subxt_signer::eth::dev::dorothy()),
463 crate::Account::from(subxt_signer::eth::dev::ethan()),
464 ]
465 } else {
466 vec![]
467 })
468 .with_allow_unprotected_txs(allow_unprotected_txs)
469 .with_use_pending_for_estimate_gas(is_dev)
470 .into_rpc();
471
472 let health_api = SystemHealthRpcServerImpl::new(client.clone()).into_rpc();
473 let debug_api = DebugRpcServerImpl::new(client.clone()).into_rpc();
474 let polkadot_api = PolkadotRpcServerImpl::new(client).into_rpc();
475
476 let mut module = RpcModule::new(());
477 module.merge(eth_api).map_err(|e| sc_service::Error::Application(e.into()))?;
478 module.merge(health_api).map_err(|e| sc_service::Error::Application(e.into()))?;
479 module.merge(debug_api).map_err(|e| sc_service::Error::Application(e.into()))?;
480 module
481 .merge(polkadot_api)
482 .map_err(|e| sc_service::Error::Application(e.into()))?;
483 Ok(module)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use tempfile::TempDir;
490
491 #[test]
492 fn in_memory_returns_memory_options() {
493 let opts = resolve_db_options(EthPruningMode::KeepLatest(256), None).unwrap();
494 let filename = opts.get_filename();
496 assert_eq!(filename, std::path::Path::new(":memory:"));
497 }
498
499 #[test]
500 fn persistent_with_explicit_base_path() {
501 let tmp = TempDir::new().unwrap();
502 let base = BasePath::new(tmp.path());
503 let opts = resolve_db_options(EthPruningMode::Archive, Some(base)).unwrap();
504 assert_eq!(opts.get_filename(), tmp.path().join(DEFAULT_DATABASE_NAME));
505 assert!(tmp.path().exists());
506 }
507
508 #[test]
509 fn persistent_default_path() {
510 let opts = resolve_db_options(EthPruningMode::Archive, None).unwrap();
511 let filename = opts.get_filename().to_string_lossy().to_string();
512 assert!(filename.contains("eth-rpc"));
513 assert!(filename.contains(DEFAULT_DATABASE_NAME));
514 }
515
516 #[test]
517 fn persistent_creates_nested_directories() {
518 let tmp = TempDir::new().unwrap();
519 let nested = tmp.path().join("a").join("b");
520 let base = BasePath::new(&nested);
521 resolve_db_options(EthPruningMode::Archive, Some(base)).unwrap();
522 assert!(nested.exists());
523 }
524
525 #[test]
526 fn eth_pruning_mode() {
527 let cmd = CliCommand::try_parse_from(["eth-rpc", "--eth-pruning", "archive"]).unwrap();
529 assert_eq!(cmd.eth_pruning, EthPruningMode::Archive);
530
531 let cmd = CliCommand::try_parse_from(["eth-rpc", "--eth-pruning", "256"]).unwrap();
532 assert_eq!(cmd.eth_pruning, EthPruningMode::KeepLatest(256));
533
534 let cmd = CliCommand::try_parse_from(["eth-rpc"]).unwrap();
536 assert_eq!(cmd.eth_pruning, EthPruningMode::Archive);
537 }
538}