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 chain_id = crate::chain_id(&api).await?;
293 let receipt_extractor = ReceiptExtractor::new(runtime_api_provider.clone(), chain_id);
294 let max_variable_number = sqlite_db_query_max_variable_number(&pool).await;
295 let db_ctx = DbContext::new(pool, max_variable_number);
296
297 let receipt_provider = ReceiptProvider::new(
298 db_ctx,
299 block_provider.clone(),
300 receipt_extractor.clone(),
301 keep_latest_n_blocks,
302 )
303 .await?;
304
305 let client = Client::new(
306 api,
307 rpc_client,
308 rpc,
309 block_provider,
310 receipt_provider,
311 eth_pruning.is_archive(),
312 subscription_gap_queue,
313 runtime_api_provider,
314 chain_id,
315 )
316 .await?;
317
318 Ok(client)
319 }
320 .fuse();
321 pin_mut!(fut);
322
323 match tokio_handle.block_on(abort_signal.try_until_signal(fut)) {
324 Ok(Ok(client)) => Ok(client),
325 Ok(Err(err)) => Err(err),
326 Err(_) => anyhow::bail!("Process interrupted"),
327 }
328}
329
330pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
332 let CliCommand {
333 rpc_params,
334 prometheus_params,
335 node_rpc_url,
336 eth_pruning,
337 shared_params,
338 allow_unprotected_txs,
339 ..
340 } = cmd;
341
342 #[cfg(not(test))]
343 init_logger(&shared_params)?;
344 let is_dev = shared_params.dev;
345 let explicit_base_path = shared_params.base_path.is_some();
346 let base_path = shared_params.base_path()?;
347
348 if is_dev && eth_pruning.is_archive() && !explicit_base_path {
349 log::warn!(
350 target: LOG_TARGET,
351 "โ ๏ธ Running in --dev mode with --eth-pruning=archive but no --base-path. \
352 The database will be stored in a temporary directory and lost on exit. \
353 Use --base-path to persist the database."
354 );
355 }
356
357 let db_options = resolve_db_options(eth_pruning, base_path)?;
358
359 let rpc_addrs: Option<Vec<sc_service::config::RpcEndpoint>> = rpc_params
360 .rpc_addr(is_dev, false, DEFAULT_RPC_PORT)?
361 .map(|addrs| addrs.into_iter().map(Into::into).collect());
362
363 let rpc_config = RpcConfiguration {
364 addr: rpc_addrs,
365 methods: rpc_params.rpc_methods.into(),
366 max_connections: rpc_params.rpc_max_connections,
367 cors: rpc_params.rpc_cors(is_dev)?,
368 max_request_size: rpc_params.rpc_max_request_size,
369 max_response_size: rpc_params.rpc_max_response_size,
370 id_provider: None,
371 max_subs_per_conn: rpc_params.rpc_max_subscriptions_per_connection,
372 port: rpc_params.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
373 message_buffer_capacity: rpc_params.rpc_message_buffer_capacity_per_connection,
374 batch_config: rpc_params.rpc_batch_config()?,
375 rate_limit: rpc_params.rpc_rate_limit,
376 rate_limit_whitelisted_ips: rpc_params.rpc_rate_limit_whitelisted_ips,
377 rate_limit_trust_proxy_headers: rpc_params.rpc_rate_limit_trust_proxy_headers,
378 request_logger_limit: if is_dev { 1024 * 1024 } else { 1024 },
379 };
380
381 let prometheus_config =
382 prometheus_params.prometheus_config(DEFAULT_PROMETHEUS_PORT, "eth-rpc".into());
383 let prometheus_registry = prometheus_config.as_ref().map(|config| &config.registry);
384
385 let tokio_runtime = sc_cli::build_runtime()?;
386 let tokio_handle = tokio_runtime.handle();
387 let mut task_manager = TaskManager::new(tokio_handle.clone(), prometheus_registry)?;
388
389 let (subscription_gap_queue, gap_fill_rx) = SubscriptionGapQueue::new();
390 let client = build_client(
391 tokio_handle,
392 eth_pruning,
393 &node_rpc_url,
394 db_options,
395 rpc_config.max_request_size * 1024 * 1024,
396 rpc_config.max_response_size * 1024 * 1024,
397 tokio_runtime.block_on(async { Signals::capture() })?,
398 subscription_gap_queue,
399 )?;
400
401 if let Some(PrometheusConfig { port, registry }) = prometheus_config.clone() {
403 task_manager.spawn_handle().spawn(
404 "prometheus-endpoint",
405 None,
406 prometheus_endpoint::init_prometheus(port, registry).map(drop),
407 );
408 }
409
410 let rpc_runtime = create_rpc_runtime(rpc_config.max_connections)
411 .map_err(|e| anyhow::anyhow!("Failed to create RPC runtime: {}", e))?;
412
413 let rpc_api = rpc_module(is_dev, client.clone(), allow_unprotected_txs)?;
414 let rpc_server_handle = start_rpc_servers(
415 &rpc_config,
416 prometheus_registry,
417 tokio_handle,
418 rpc_api,
419 rpc_runtime,
420 None,
421 )?;
422
423 task_manager
424 .spawn_essential_handle()
425 .spawn("block-subscription", None, async move {
426 let mut futures: Vec<BoxFuture<'_, Result<(), _>>> = vec![
427 Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::BestBlocks)),
428 Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::FinalizedBlocks)),
429 ];
430
431 if eth_pruning.is_archive() {
432 futures.push(Box::pin(client.sync_backward()));
433 }
434
435 futures.push(Box::pin(async {
437 client.run_subscription_gap_filler(gap_fill_rx).await;
438 Ok::<_, ClientError>(())
439 }));
440
441 if let Err(err) = futures::future::try_join_all(futures).await {
442 panic!("Block subscription task failed: {err:?}",)
443 }
444 });
445
446 task_manager.keep_alive(rpc_server_handle);
447 let signals = tokio_runtime.block_on(async { Signals::capture() })?;
448 tokio_runtime.block_on(signals.run_until_signal(task_manager.future().fuse()))?;
449 Ok(())
450}
451
452fn rpc_module(
454 is_dev: bool,
455 client: Client,
456 allow_unprotected_txs: bool,
457) -> Result<RpcModule<()>, sc_service::Error> {
458 let eth_api = EthRpcServerImpl::new(client.clone())
459 .with_accounts(if is_dev {
460 vec![
461 crate::Account::from(subxt_signer::eth::dev::alith()),
462 crate::Account::from(subxt_signer::eth::dev::baltathar()),
463 crate::Account::from(subxt_signer::eth::dev::charleth()),
464 crate::Account::from(subxt_signer::eth::dev::dorothy()),
465 crate::Account::from(subxt_signer::eth::dev::ethan()),
466 ]
467 } else {
468 vec![]
469 })
470 .with_allow_unprotected_txs(allow_unprotected_txs)
471 .with_use_pending_for_estimate_gas(is_dev)
472 .into_rpc();
473
474 let health_api = SystemHealthRpcServerImpl::new(client.clone()).into_rpc();
475 let debug_api = DebugRpcServerImpl::new(client.clone()).into_rpc();
476 let polkadot_api = PolkadotRpcServerImpl::new(client).into_rpc();
477
478 let mut module = RpcModule::new(());
479 module.merge(eth_api).map_err(|e| sc_service::Error::Application(e.into()))?;
480 module.merge(health_api).map_err(|e| sc_service::Error::Application(e.into()))?;
481 module.merge(debug_api).map_err(|e| sc_service::Error::Application(e.into()))?;
482 module
483 .merge(polkadot_api)
484 .map_err(|e| sc_service::Error::Application(e.into()))?;
485 Ok(module)
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use tempfile::TempDir;
492
493 #[test]
494 fn in_memory_returns_memory_options() {
495 let opts = resolve_db_options(EthPruningMode::KeepLatest(256), None).unwrap();
496 let filename = opts.get_filename();
498 assert_eq!(filename, std::path::Path::new(":memory:"));
499 }
500
501 #[test]
502 fn persistent_with_explicit_base_path() {
503 let tmp = TempDir::new().unwrap();
504 let base = BasePath::new(tmp.path());
505 let opts = resolve_db_options(EthPruningMode::Archive, Some(base)).unwrap();
506 assert_eq!(opts.get_filename(), tmp.path().join(DEFAULT_DATABASE_NAME));
507 assert!(tmp.path().exists());
508 }
509
510 #[test]
511 fn persistent_default_path() {
512 let opts = resolve_db_options(EthPruningMode::Archive, None).unwrap();
513 let filename = opts.get_filename().to_string_lossy().to_string();
514 assert!(filename.contains("eth-rpc"));
515 assert!(filename.contains(DEFAULT_DATABASE_NAME));
516 }
517
518 #[test]
519 fn persistent_creates_nested_directories() {
520 let tmp = TempDir::new().unwrap();
521 let nested = tmp.path().join("a").join("b");
522 let base = BasePath::new(&nested);
523 resolve_db_options(EthPruningMode::Archive, Some(base)).unwrap();
524 assert!(nested.exists());
525 }
526
527 #[test]
528 fn eth_pruning_mode() {
529 let cmd = CliCommand::try_parse_from(["eth-rpc", "--eth-pruning", "archive"]).unwrap();
531 assert_eq!(cmd.eth_pruning, EthPruningMode::Archive);
532
533 let cmd = CliCommand::try_parse_from(["eth-rpc", "--eth-pruning", "256"]).unwrap();
534 assert_eq!(cmd.eth_pruning, EthPruningMode::KeepLatest(256));
535
536 let cmd = CliCommand::try_parse_from(["eth-rpc"]).unwrap();
538 assert_eq!(cmd.eth_pruning, EthPruningMode::Archive);
539 }
540}