referrerpolicy=no-referrer-when-downgrade

pallet_revive_eth_rpc/
receipt_provider.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17use crate::{
18	Address, BlockInfoProvider, BlockNumberOrTag, Bytes, ChainMetadata, ClientError, Filter,
19	FilterBlockOption, Log, ReceiptExtractor, ReceiptInfo, SubxtBlockInfoProvider, SyncLabel,
20	SyncStateKey,
21	block_sync::SyncCheckpoint,
22	client::{SubstrateBlock, SubstrateBlockNumber},
23};
24use futures::future::OptionFuture;
25use pallet_revive::evm::TransactionSigned;
26use sp_core::{H256, U256};
27use sqlx::{QueryBuilder, Row, Sqlite, SqlitePool, query};
28use std::{
29	collections::{BTreeMap, HashMap},
30	sync::Arc,
31};
32use tokio::sync::Mutex;
33
34const LOG_TARGET: &str = "eth-rpc::receipt_provider";
35const MAX_LOG_RESULTS: usize = 10_000;
36
37/// Parse a SQLite row from the `logs` table into a [`Log`].
38fn parse_log_row(row: sqlx::sqlite::SqliteRow) -> Result<Log, sqlx::Error> {
39	let block_hash: Vec<u8> = row.try_get("block_hash")?;
40	let transaction_index: i64 = row.try_get("transaction_index")?;
41	let log_index: i64 = row.try_get("log_index")?;
42	let address: Vec<u8> = row.try_get("address")?;
43	let block_number: i64 = row.try_get("block_number")?;
44	let transaction_hash: Vec<u8> = row.try_get("transaction_hash")?;
45	let topic_0: Option<Vec<u8>> = row.try_get("topic_0")?;
46	let topic_1: Option<Vec<u8>> = row.try_get("topic_1")?;
47	let topic_2: Option<Vec<u8>> = row.try_get("topic_2")?;
48	let topic_3: Option<Vec<u8>> = row.try_get("topic_3")?;
49	let data: Option<Vec<u8>> = row.try_get("data")?;
50
51	let topics = [topic_0, topic_1, topic_2, topic_3]
52		.iter()
53		.filter_map(|t| t.as_ref().map(|t| H256::from_slice(t)))
54		.collect::<Vec<_>>();
55
56	Ok(Log {
57		address: Address::from_slice(&address),
58		block_hash: H256::from_slice(&block_hash),
59		block_number: U256::from(block_number as u64),
60		data: data.map(Bytes::from),
61		log_index: U256::from(log_index as u64),
62		topics,
63		transaction_hash: H256::from_slice(&transaction_hash),
64		transaction_index: U256::from(transaction_index as u64),
65		removed: false,
66	})
67}
68
69/// SQLite connection pool with precomputed bulk-insert chunk sizes.
70#[derive(Clone)]
71pub struct DbContext {
72	pool: SqlitePool,
73	/// Max bound parameters per query.
74	max_variable_number: usize,
75	/// Chunk size for bulk INSERT into `transaction_hashes`.
76	tx_insert_chunk_size: usize,
77	/// Chunk size for bulk INSERT into `logs`.
78	log_insert_chunk_size: usize,
79}
80
81impl DbContext {
82	/// Conservative default for `SQLITE_LIMIT_VARIABLE_NUMBER`; SQLite >=3.32 uses 32766.
83	pub const DEFAULT_MAX_VARIABLE_NUMBER: usize = 999;
84	/// Columns in the `transaction_hashes` table.
85	const TX_HASH_COLUMNS: usize = 3;
86	/// Columns in the `logs` table.
87	const LOG_COLUMNS: usize = 11;
88
89	pub fn new(pool: SqlitePool, max_variable_number: usize) -> Self {
90		assert!(
91			max_variable_number >= Self::LOG_COLUMNS,
92			"SQLite max_variable_number ({max_variable_number}) must be >= {}",
93			Self::LOG_COLUMNS
94		);
95		Self {
96			pool,
97			max_variable_number,
98			tx_insert_chunk_size: max_variable_number / Self::TX_HASH_COLUMNS,
99			log_insert_chunk_size: max_variable_number / Self::LOG_COLUMNS,
100		}
101	}
102}
103
104/// ReceiptProvider stores transaction receipts and logs in a SQLite database.
105#[derive(Clone)]
106pub struct ReceiptProvider<B: BlockInfoProvider = SubxtBlockInfoProvider> {
107	/// The database pool.
108	db_ctx: DbContext,
109	/// The block provider used to fetch blocks, and reconstruct receipts.
110	block_provider: B,
111	/// A means to extract receipts from extrinsics.
112	receipt_extractor: ReceiptExtractor,
113	/// When `Some`, old blocks will be pruned.
114	keep_latest_n_blocks: Option<usize>,
115	/// A Map of the latest block numbers to block hashes.
116	block_number_to_hashes: Arc<Mutex<BTreeMap<SubstrateBlockNumber, BlockHashMap>>>,
117}
118
119/// Substrate block to Ethereum block mapping
120#[derive(Clone, Debug, PartialEq, Eq)]
121struct BlockHashMap {
122	substrate_hash: H256,
123	ethereum_hash: H256,
124}
125
126impl BlockHashMap {
127	fn new(substrate_hash: H256, ethereum_hash: H256) -> Self {
128		Self { substrate_hash, ethereum_hash }
129	}
130}
131
132/// Provides information about a block,
133/// This is an abstraction on top of [`SubstrateBlock`] that can't be mocked in tests.
134/// Can be removed once <https://github.com/paritytech/subxt/issues/1883> is fixed.
135pub trait BlockInfo {
136	/// Returns the block hash.
137	fn hash(&self) -> H256;
138	/// Returns the block number.
139	fn number(&self) -> SubstrateBlockNumber;
140}
141
142impl BlockInfo for SubstrateBlock {
143	fn hash(&self) -> H256 {
144		self.block_hash()
145	}
146	fn number(&self) -> SubstrateBlockNumber {
147		self.block_number()
148	}
149}
150
151/// Maximum number of entries kept in the block to hash map.
152pub const MAX_CACHED_BLOCKS: usize = 256;
153
154/// Upsert a sync label row, updating only when the existing `block_number`
155/// compares with `$op` against the new value. `$op` must be `"<"` or `">"`.
156macro_rules! upsert_sync_label {
157	($pool:expr, $op:literal, $label:expr, $checkpoint:expr) => {{
158		let label_str = $label.to_string();
159		let block_number = $checkpoint.block_number as i64;
160		let block_hash = $checkpoint.block_hash.map(|h| h.as_bytes().to_vec());
161		query!(
162			"INSERT INTO sync_state (label, block_number, block_hash)
163			VALUES ($1, $2, $3)
164			ON CONFLICT(label) DO UPDATE
165				SET block_number = excluded.block_number, block_hash = excluded.block_hash
166			WHERE sync_state.block_number " +
167				$op + " excluded.block_number
168			",
169			label_str,
170			block_number,
171			block_hash
172		)
173		.execute($pool)
174		.await?;
175	}};
176}
177
178async fn insert_block_mapping<'e, E: sqlx::Executor<'e, Database = Sqlite>>(
179	executor: E,
180	block_map: &BlockHashMap,
181) -> Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> {
182	let ethereum_hash_ref = block_map.ethereum_hash.as_ref();
183	let substrate_hash_ref = block_map.substrate_hash.as_ref();
184	query!(
185		r#"
186			INSERT OR REPLACE INTO eth_to_substrate_blocks (ethereum_block_hash, substrate_block_hash)
187			VALUES ($1, $2)
188			"#,
189		ethereum_hash_ref,
190		substrate_hash_ref,
191	)
192	.execute(executor)
193	.await
194}
195
196impl<B: BlockInfoProvider> ReceiptProvider<B> {
197	/// Create a new `ReceiptProvider` with the given database URL and block provider.
198	pub async fn new(
199		db_ctx: DbContext,
200		block_provider: B,
201		receipt_extractor: ReceiptExtractor,
202		keep_latest_n_blocks: Option<usize>,
203	) -> Result<Self, ClientError> {
204		sqlx::migrate!()
205			.run(&db_ctx.pool)
206			.await
207			.map_err(|e| sqlx::Error::Migrate(e.into()))?;
208
209		let provider = Self {
210			db_ctx,
211			block_provider,
212			receipt_extractor,
213			keep_latest_n_blocks,
214			block_number_to_hashes: Default::default(),
215		};
216		provider.restore_first_evm_block().await?;
217
218		Ok(provider)
219	}
220
221	/// Returns `true` if the block is before the auto-discovered `first_evm_block`.
222	pub fn is_before_earliest_block(&self, at: &BlockNumberOrTag) -> bool {
223		match at {
224			BlockNumberOrTag::Number(block_number) => {
225				self.receipt_extractor.is_before_first_evm_block(*block_number)
226			},
227			BlockNumberOrTag::Latest |
228			BlockNumberOrTag::Finalized |
229			BlockNumberOrTag::Safe |
230			BlockNumberOrTag::Earliest |
231			BlockNumberOrTag::Pending => false,
232		}
233	}
234
235	/// The auto-discovered first EVM block, or `None` if not yet discovered.
236	pub fn first_evm_block(&self) -> Option<SubstrateBlockNumber> {
237		self.receipt_extractor.first_evm_block()
238	}
239
240	/// Set the auto-discovered first EVM block (in-memory + persisted to DB).
241	pub async fn set_first_evm_block(
242		&self,
243		block_number: SubstrateBlockNumber,
244	) -> Result<(), ClientError> {
245		self.receipt_extractor.set_first_evm_block(block_number);
246		self.set_sync_label(ChainMetadata::FirstEvmBlock, SyncCheckpoint::from_number(block_number))
247			.await
248	}
249
250	/// Restore `first_evm_block` from DB, clearing it if the boundary has shifted.
251	async fn restore_first_evm_block(&self) -> Result<(), ClientError> {
252		let Some(evm_first) =
253			self.get_sync_label(ChainMetadata::FirstEvmBlock).await?.map(|c| c.block_number)
254		else {
255			return Ok(());
256		};
257
258		let has_evm_hash = |block_number: SubstrateBlockNumber| async move {
259			match self.block_provider.block_by_number(block_number).await.ok().flatten() {
260				Some(block) => self
261					.receipt_extractor
262					.get_ethereum_block_hash(&block.hash(), block_number)
263					.await
264					.is_some(),
265				None => false,
266			}
267		};
268
269		// Stale if evm_first no longer has an EVM hash, or its predecessor now does.
270		let current_has_evm = has_evm_hash(evm_first).await;
271		let predecessor_has_evm =
272			if evm_first > 0 { has_evm_hash(evm_first - 1).await } else { false };
273
274		if !current_has_evm || predecessor_has_evm {
275			log::warn!(target: LOG_TARGET,
276				"๐Ÿ—„๏ธ Stored first-evm-block=#{evm_first} is stale \
277				 (has_evm={current_has_evm}, predecessor_has_evm={predecessor_has_evm}), \
278				 clearing.");
279			if let Err(e) = self.delete_sync_label(ChainMetadata::FirstEvmBlock).await {
280				log::error!(target: LOG_TARGET,
281					"๐Ÿ—„๏ธ Failed to clear stale first-evm-block from DB: {e:?}");
282			}
283		} else {
284			self.receipt_extractor.set_first_evm_block(evm_first);
285		}
286		Ok(())
287	}
288
289	// Get block hash and transaction index by transaction hash
290	pub async fn find_transaction(&self, transaction_hash: &H256) -> Option<(H256, usize)> {
291		let transaction_hash_bytes = transaction_hash.as_ref();
292		let result = query!(
293			r#"
294			SELECT block_hash, transaction_index
295			FROM transaction_hashes
296			WHERE transaction_hash = $1
297			"#,
298			transaction_hash_bytes
299		)
300		.fetch_optional(&self.db_ctx.pool)
301		.await
302		.inspect_err(|err| {
303			log::trace!(target: LOG_TARGET,
304				"find_transaction: DB query failed for tx {transaction_hash:?}: {err:?}");
305		})
306		.ok()?
307		.or_else(|| {
308			log::trace!(target: LOG_TARGET,
309				"find_transaction: tx {transaction_hash:?} not found in DB");
310			None
311		})?;
312
313		let block_hash = H256::from_slice(&result.block_hash[..]);
314		let transaction_index = result.transaction_index.try_into().ok()?;
315		Some((block_hash, transaction_index))
316	}
317
318	/// Get the Substrate block hash for the given Ethereum block hash.
319	pub async fn get_substrate_hash(&self, ethereum_block_hash: &H256) -> Option<H256> {
320		let ethereum_hash = ethereum_block_hash.as_ref();
321		let result = query!(
322			r#"
323			SELECT substrate_block_hash
324			FROM eth_to_substrate_blocks
325			WHERE ethereum_block_hash = $1
326			"#,
327			ethereum_hash
328		)
329		.fetch_optional(&self.db_ctx.pool)
330		.await
331		.inspect_err(|e| {
332			log::error!(target: LOG_TARGET, "failed to get block mapping for ethereum block {ethereum_block_hash:?}, err: {e:?}");
333		})
334		.ok()?
335		.or_else(||{
336			log::trace!(target: LOG_TARGET, "No block mapping found for ethereum block: {ethereum_block_hash:?}");
337			None
338		})?;
339
340		log::trace!(target: LOG_TARGET, "Get block mapping ethereum block: {:?} -> substrate block: {ethereum_block_hash:?}", H256::from_slice(&result.substrate_block_hash[..]));
341
342		Some(H256::from_slice(&result.substrate_block_hash[..]))
343	}
344
345	/// Get the Ethereum block hash for the given Substrate block hash.
346	pub async fn get_ethereum_hash(&self, substrate_block_hash: &H256) -> Option<H256> {
347		let substrate_hash = substrate_block_hash.as_ref();
348		let result = query!(
349			r#"
350			SELECT ethereum_block_hash
351			FROM eth_to_substrate_blocks
352			WHERE substrate_block_hash = $1
353			"#,
354			substrate_hash
355		)
356		.fetch_optional(&self.db_ctx.pool)
357		.await
358		.inspect_err(|e| {
359			log::error!(target: LOG_TARGET, "failed to get block mapping for substrate block {substrate_block_hash:?}, err: {e:?}");
360		})
361		.ok()?
362		.or_else(||{
363			log::trace!(target: LOG_TARGET, "No block mapping found for substrate block: {substrate_block_hash:?}");
364			None
365		})?;
366
367		log::trace!(target: LOG_TARGET, "Get block mapping substrate block: {substrate_block_hash:?} -> ethereum block: {:?}", H256::from_slice(&result.ethereum_block_hash[..]));
368
369		Some(H256::from_slice(&result.ethereum_block_hash[..]))
370	}
371
372	/// Deletes older records from the database.
373	async fn remove(&self, block_mappings: &[BlockHashMap]) -> Result<(), ClientError> {
374		if block_mappings.is_empty() {
375			return Ok(());
376		}
377		log::debug!(target: LOG_TARGET, "Removing block hashes: {block_mappings:?}");
378
379		let mut db_tx = self.db_ctx.pool.begin().await?;
380
381		for chunk in block_mappings.chunks(self.db_ctx.max_variable_number) {
382			let placeholders = vec!["?"; chunk.len()].join(", ");
383			let sql_tx =
384				format!("DELETE FROM transaction_hashes WHERE block_hash in ({placeholders})");
385			let sql_logs = format!("DELETE FROM logs WHERE block_hash in ({placeholders})");
386			let sql_mappings = format!(
387				"DELETE FROM eth_to_substrate_blocks WHERE substrate_block_hash in ({placeholders})"
388			);
389
390			let mut delete_tx_query = sqlx::query(&sql_tx);
391			let mut delete_logs_query = sqlx::query(&sql_logs);
392			let mut delete_mappings_query = sqlx::query(&sql_mappings);
393
394			for block_map in chunk {
395				delete_tx_query = delete_tx_query.bind(block_map.substrate_hash.as_ref());
396				delete_logs_query = delete_logs_query.bind(block_map.ethereum_hash.as_ref());
397				delete_mappings_query =
398					delete_mappings_query.bind(block_map.substrate_hash.as_ref());
399			}
400
401			delete_tx_query.execute(&mut *db_tx).await?;
402			delete_logs_query.execute(&mut *db_tx).await?;
403			delete_mappings_query.execute(&mut *db_tx).await?;
404		}
405
406		db_tx.commit().await?;
407		Ok(())
408	}
409
410	/// Read a sync label entry.
411	pub async fn get_sync_label(
412		&self,
413		label: impl SyncStateKey,
414	) -> Result<Option<SyncCheckpoint>, ClientError> {
415		let label_str = label.to_string();
416		let row = query!(
417			r#"
418			SELECT block_number, block_hash
419			FROM sync_state
420			WHERE label = $1
421			"#,
422			label_str
423		)
424		.fetch_optional(&self.db_ctx.pool)
425		.await?;
426
427		match row {
428			Some(row) => {
429				let block_number: SubstrateBlockNumber =
430					row.block_number.try_into().map_err(|_| {
431						sqlx::Error::Decode(
432							format!("block_number {} overflows u32", row.block_number).into(),
433						)
434					})?;
435				Ok(Some(SyncCheckpoint {
436					block_number,
437					block_hash: row
438						.block_hash
439						.filter(|b| b.len() == 32)
440						.map(|b| H256::from_slice(&b)),
441				}))
442			},
443			None => Ok(None),
444		}
445	}
446
447	/// Upsert a sync label entry.
448	pub async fn set_sync_label(
449		&self,
450		label: impl SyncStateKey,
451		checkpoint: SyncCheckpoint,
452	) -> Result<(), ClientError> {
453		let label_str = label.to_string();
454		let block_number = checkpoint.block_number as i64;
455		let block_hash = checkpoint.block_hash.map(|h| h.as_bytes().to_vec());
456		query!(
457			r#"
458			INSERT OR REPLACE INTO sync_state (label, block_number, block_hash)
459			VALUES ($1, $2, $3)
460			"#,
461			label_str,
462			block_number,
463			block_hash,
464		)
465		.execute(&self.db_ctx.pool)
466		.await?;
467		Ok(())
468	}
469
470	/// Delete a sync label entry.
471	pub async fn delete_sync_label(&self, label: impl SyncStateKey) -> Result<(), ClientError> {
472		let label_str = label.to_string();
473		query!(
474			r#"
475			DELETE FROM sync_state WHERE label = $1
476			"#,
477			label_str,
478		)
479		.execute(&self.db_ctx.pool)
480		.await?;
481		Ok(())
482	}
483
484	/// Atomically update a sync label entry only if the new block number is strictly higher.
485	///
486	/// Inserts the row if it doesn't exist yet.
487	pub async fn advance_sync_label(
488		&self,
489		label: SyncLabel,
490		checkpoint: SyncCheckpoint,
491	) -> Result<(), ClientError> {
492		upsert_sync_label!(&self.db_ctx.pool, "<", label, checkpoint);
493		Ok(())
494	}
495
496	/// Atomically update a sync label entry only if the new block number is lower.
497	///
498	/// Inserts the row if it doesn't exist yet.
499	pub async fn recede_sync_label(
500		&self,
501		label: SyncLabel,
502		checkpoint: SyncCheckpoint,
503	) -> Result<(), ClientError> {
504		upsert_sync_label!(&self.db_ctx.pool, ">", label, checkpoint);
505		Ok(())
506	}
507
508	/// Look up the ethereum block hash for a previously processed block from the in-memory cache.
509	pub async fn get_processed_eth_block_hash(
510		&self,
511		block_number: SubstrateBlockNumber,
512		substrate_hash: H256,
513	) -> Option<H256> {
514		self.block_number_to_hashes
515			.lock()
516			.await
517			.get(&block_number)
518			.filter(|entry| entry.substrate_hash == substrate_hash)
519			.map(|entry| entry.ethereum_hash)
520	}
521
522	/// Fetch receipts from the given block, using a pre-fetched ethereum block hash.
523	pub async fn receipts_from_block(
524		&self,
525		block: &SubstrateBlock,
526		ethereum_hash: H256,
527	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
528		self.receipt_extractor
529			.extract_from_block_with_eth_hash(block, ethereum_hash)
530			.await
531	}
532
533	/// Fetch all receipts for the given block.
534	pub async fn block_receipts(
535		&self,
536		block: &SubstrateBlock,
537	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
538		self.receipt_extractor.extract_from_block(block).await
539	}
540
541	/// Like [`Self::insert_block_receipts`] but writes only to the DB (no cache update).
542	/// Used for historic sync where fork detection is unnecessary.
543	pub async fn insert_block_receipts_past(
544		&self,
545		block: &SubstrateBlock,
546		ethereum_hash: &H256,
547	) -> Result<(), ClientError> {
548		let receipts = self
549			.receipt_extractor
550			.extract_from_block_with_eth_hash(block, *ethereum_hash)
551			.await?;
552		self.insert_into_db(block, &receipts, ethereum_hash).await?;
553		Ok(())
554	}
555
556	/// Insert pre-extracted receipts and update the block cache (with fork detection).
557	pub async fn insert_block_receipts(
558		&self,
559		block: &SubstrateBlock,
560		receipts: &[(TransactionSigned, ReceiptInfo)],
561		ethereum_hash: &H256,
562	) -> Result<(), ClientError> {
563		self.insert(block, receipts, ethereum_hash).await
564	}
565
566	/// Insert receipts into the provider, updating the in-memory block cache for fork detection.
567	///
568	/// Note: Can be merged into `insert_block_receipts` once <https://github.com/paritytech/subxt/issues/1883> is fixed and subxt let
569	/// us create Mock `SubstrateBlock`
570	async fn insert(
571		&self,
572		block: &impl BlockInfo,
573		receipts: &[(TransactionSigned, ReceiptInfo)],
574		ethereum_hash: &H256,
575	) -> Result<(), ClientError> {
576		let block_map = BlockHashMap::new(block.hash(), *ethereum_hash);
577		self.prune_blocks(block.number(), &block_map).await?;
578		self.insert_into_db(block, receipts, ethereum_hash).await?;
579		Ok(())
580	}
581
582	/// Handle fork detection (always) and DB pruning (temporary mode only).
583	async fn prune_blocks(
584		&self,
585		block_number: SubstrateBlockNumber,
586		block_map: &BlockHashMap,
587	) -> Result<(), ClientError> {
588		let mut to_remove = Vec::new();
589		let mut block_number_to_hash = self.block_number_to_hashes.lock().await;
590
591		// Fork? - If inserting the same block number with a different hash, remove the old ones.
592		match block_number_to_hash.insert(block_number, block_map.clone()) {
593			Some(old_block_map) if &old_block_map != block_map => {
594				to_remove.push(old_block_map);
595
596				// Now loop through the blocks that were building on top of the old fork and remove
597				// them.
598				let mut next_block_number = block_number.saturating_add(1);
599				while let Some(old_block_map) = block_number_to_hash.remove(&next_block_number) {
600					to_remove.push(old_block_map);
601					next_block_number = next_block_number.saturating_add(1);
602				}
603			},
604			_ => {},
605		}
606
607		if let Some(keep_latest_n_blocks) = self.keep_latest_n_blocks {
608			// If we have more blocks than we should keep, remove the oldest ones by count
609			// (not by block number range, to handle gaps correctly)
610			while block_number_to_hash.len() > keep_latest_n_blocks {
611				// Remove the block with the smallest number (first in BTreeMap)
612				if let Some((_, block_map)) = block_number_to_hash.pop_first() {
613					to_remove.push(block_map);
614				}
615			}
616		} else {
617			// Evict oldest entries to prevent unbounded growth.
618			// Forks deeper than MAX_CACHED_BLOCKS(256) are unlikely.
619			while block_number_to_hash.len() > MAX_CACHED_BLOCKS {
620				block_number_to_hash.pop_first();
621			}
622		}
623
624		// Release the lock.
625		drop(block_number_to_hash);
626
627		if !to_remove.is_empty() {
628			log::trace!(target: LOG_TARGET, "Pruning old blocks: {to_remove:?}");
629			self.remove(&to_remove).await?;
630		}
631
632		Ok(())
633	}
634
635	/// Insert receipts into the database without updating the in-memory block cache.
636	async fn insert_into_db(
637		&self,
638		block: &impl BlockInfo,
639		receipts: &[(TransactionSigned, ReceiptInfo)],
640		ethereum_hash: &H256,
641	) -> Result<(), ClientError> {
642		let block_number = block.number() as i64;
643		let substrate_block_hash = block.hash();
644		let substrate_hash_ref = substrate_block_hash.as_ref();
645		let ethereum_hash_ref = ethereum_hash.as_ref();
646
647		log::trace!(target: LOG_TARGET, "Inserting receipts for block #{block_number} ethereum: {ethereum_hash:?} substrate: {substrate_block_hash:?}");
648
649		// Check if mapping already exists (eg. added when processing best block and we are now
650		// processing finalized block)
651		let result = sqlx::query!(
652			r#"SELECT EXISTS(SELECT 1 FROM eth_to_substrate_blocks WHERE substrate_block_hash = $1) AS "exists!:bool""#, substrate_hash_ref
653		)
654		.fetch_one(&self.db_ctx.pool)
655		.await?;
656
657		// Assuming that if no mapping exists then no relevant entries in transaction_hashes and
658		// logs exist
659		if result.exists {
660			log::trace!(target: LOG_TARGET,
661				"Skipping receipt insert for block #{block_number} ({substrate_block_hash:?}): \
662				 mapping already exists. ETH hash: {ethereum_hash:?}, receipts count: {count}",
663				count = receipts.len(),
664			);
665			return Ok(());
666		}
667
668		let mut db_tx = self.db_ctx.pool.begin().await?;
669
670		for chunk in receipts.chunks(self.db_ctx.tx_insert_chunk_size) {
671			let mut query_builder = QueryBuilder::<Sqlite>::new(
672				"INSERT OR REPLACE INTO transaction_hashes (transaction_hash, block_hash, transaction_index) ",
673			);
674			query_builder.push_values(chunk, |mut row, (_, receipt)| {
675				row.push_bind(receipt.transaction_hash.as_ref() as &[u8])
676					.push_bind(substrate_hash_ref)
677					.push_bind(receipt.transaction_index.as_u32() as i32);
678			});
679			query_builder.build().execute(&mut *db_tx).await?;
680		}
681
682		let all_logs: Vec<(i32, &[u8], &Log)> = receipts
683			.iter()
684			.flat_map(|(_, receipt)| {
685				let tx_index = receipt.transaction_index.as_u32() as i32;
686				let tx_hash: &[u8] = receipt.transaction_hash.as_ref();
687				receipt.logs.iter().map(move |log| (tx_index, tx_hash, log))
688			})
689			.collect();
690
691		for chunk in all_logs.chunks(self.db_ctx.log_insert_chunk_size) {
692			let mut query_builder = QueryBuilder::<Sqlite>::new(
693				"INSERT OR REPLACE INTO logs(block_hash, transaction_index, log_index, address, block_number, transaction_hash, topic_0, topic_1, topic_2, topic_3, data) ",
694			);
695			query_builder.push_values(chunk, |mut row, (tx_index, tx_hash, log)| {
696				row.push_bind(ethereum_hash_ref)
697					.push_bind(*tx_index)
698					.push_bind(log.log_index.as_u32() as i32)
699					.push_bind(log.address.as_ref() as &[u8])
700					.push_bind(block_number)
701					.push_bind(*tx_hash)
702					.push_bind(log.topics.first().map(|v| &v[..]))
703					.push_bind(log.topics.get(1).map(|v| &v[..]))
704					.push_bind(log.topics.get(2).map(|v| &v[..]))
705					.push_bind(log.topics.get(3).map(|v| &v[..]))
706					.push_bind(log.data.as_ref().map(|v| &v.0[..]));
707			});
708			query_builder.build().execute(&mut *db_tx).await?;
709		}
710
711		let block_map = BlockHashMap::new(substrate_block_hash, *ethereum_hash);
712		insert_block_mapping(&mut *db_tx, &block_map).await?;
713
714		db_tx.commit().await?;
715		log::trace!(target: LOG_TARGET, "Inserted {} receipts for block #{block_number} ethereum: {ethereum_hash:?} substrate: {substrate_block_hash:?}", receipts.len());
716
717		Ok(())
718	}
719
720	/// Get logs that match the given filter.
721	///
722	/// `resolve_block_number` converts a [`BlockNumberOrTag`] to a concrete block number.
723	pub async fn logs<Fut>(
724		&self,
725		filter: Option<Filter>,
726		resolve_block_number: impl Fn(BlockNumberOrTag) -> Fut,
727	) -> anyhow::Result<Vec<Log>>
728	where
729		Fut: Future<Output = anyhow::Result<U256>>,
730	{
731		let mut qb = QueryBuilder::<Sqlite>::new("SELECT logs.* FROM logs WHERE 1=1");
732		let filter = filter.unwrap_or_default();
733
734		match filter.block_option {
735			FilterBlockOption::AtBlockHash(hash) => {
736				qb.push(" AND block_hash = ").push_bind(hash.as_slice().to_vec());
737			},
738			FilterBlockOption::Range { from_block, to_block } => {
739				let from_block =
740					OptionFuture::from(from_block.map(&resolve_block_number)).await.transpose()?;
741				let to_block =
742					OptionFuture::from(to_block.map(&resolve_block_number)).await.transpose()?;
743
744				// Read the latest block *after* resolving the tags.
745				let latest_block = U256::from(self.block_provider.latest_block_number().await);
746
747				match (from_block, to_block) {
748					(Some(block), _) | (_, Some(block)) if block > latest_block => {
749						anyhow::bail!("block number exceeds latest block");
750					},
751					(Some(from_block), Some(to_block)) if from_block > to_block => {
752						anyhow::bail!("invalid block range params");
753					},
754					(Some(from_block), Some(to_block)) if from_block == to_block => {
755						qb.push(" AND block_number = ").push_bind(from_block.as_u64() as i64);
756					},
757					(Some(from_block), Some(to_block)) => {
758						qb.push(" AND block_number BETWEEN ")
759							.push_bind(from_block.as_u64() as i64)
760							.push(" AND ")
761							.push_bind(to_block.as_u64() as i64);
762					},
763					(Some(from_block), None) => {
764						qb.push(" AND block_number >= ").push_bind(from_block.as_u64() as i64);
765					},
766					(None, Some(to_block)) => {
767						qb.push(" AND block_number <= ").push_bind(to_block.as_u64() as i64);
768					},
769					(None, None) => {
770						qb.push(" AND block_number = ").push_bind(latest_block.as_u64() as i64);
771					},
772				}
773			},
774		}
775
776		if !filter.address.is_empty() {
777			qb.push(" AND address IN (");
778			let mut separated = qb.separated(", ");
779			for addr in filter.address {
780				separated.push_bind(addr.as_slice().to_vec());
781			}
782			separated.push_unseparated(")");
783		}
784
785		for (i, topic) in filter.topics.into_iter().enumerate() {
786			if topic.is_empty() {
787				continue;
788			}
789
790			qb.push(format_args!(" AND topic_{i} IN ("));
791			let mut separated = qb.separated(", ");
792			for hash in topic {
793				separated.push_bind(hash.as_slice().to_vec());
794			}
795			separated.push_unseparated(")");
796		}
797
798		qb.push(" ORDER BY block_number, log_index LIMIT ")
799			.push_bind(MAX_LOG_RESULTS as i64);
800
801		let logs = qb.build().try_map(parse_log_row).fetch_all(&self.db_ctx.pool).await?;
802
803		if logs.len() == MAX_LOG_RESULTS {
804			log::warn!(
805				target: LOG_TARGET,
806				"Log query hit limit of {MAX_LOG_RESULTS}; results may be truncated",
807			);
808		}
809
810		Ok(logs)
811	}
812
813	/// Fetch all logs for a given block from the database.
814	pub async fn logs_by_block_number(
815		&self,
816		block_number: SubstrateBlockNumber,
817		ethereum_hash: H256,
818	) -> Result<Vec<Log>, ClientError> {
819		let mut query_builder =
820			QueryBuilder::<Sqlite>::new("SELECT logs.* FROM logs WHERE block_number = ");
821		query_builder
822			.push_bind(block_number as i64)
823			.push(" AND block_hash = ")
824			.push_bind(ethereum_hash.as_bytes().to_vec())
825			.push(" ORDER BY log_index LIMIT ")
826			.push_bind(MAX_LOG_RESULTS as i64);
827
828		let logs = query_builder
829			.build()
830			.try_map(parse_log_row)
831			.fetch_all(&self.db_ctx.pool)
832			.await?;
833
834		if logs.len() == MAX_LOG_RESULTS {
835			log::warn!(
836				target: LOG_TARGET,
837				"Log query for block {block_number} hit limit of {MAX_LOG_RESULTS}; results may be truncated",
838			);
839		}
840
841		Ok(logs)
842	}
843
844	/// Get the number of receipts per block.
845	pub async fn receipts_count_per_block(&self, block_hash: &H256) -> Option<usize> {
846		let block_hash = block_hash.as_ref();
847		let row = query!(
848			r#"
849            SELECT COUNT(*) as count
850            FROM transaction_hashes
851            WHERE block_hash = $1
852            "#,
853			block_hash
854		)
855		.fetch_one(&self.db_ctx.pool)
856		.await
857		.ok()?;
858
859		let count = row.count as usize;
860		Some(count)
861	}
862
863	/// Return all transaction hashes for the given block hash.
864	pub async fn block_transaction_hashes(
865		&self,
866		block_hash: &H256,
867	) -> Option<HashMap<usize, H256>> {
868		let block_hash = block_hash.as_ref();
869		let rows = query!(
870			r#"
871		      SELECT transaction_index, transaction_hash
872		      FROM transaction_hashes
873		      WHERE block_hash = $1
874		      "#,
875			block_hash
876		)
877		.map(|row| {
878			let transaction_index = row.transaction_index as usize;
879			let transaction_hash = H256::from_slice(&row.transaction_hash);
880			(transaction_index, transaction_hash)
881		})
882		.fetch_all(&self.db_ctx.pool)
883		.await
884		.ok()?;
885
886		Some(rows.into_iter().collect())
887	}
888
889	/// Get the receipt for the given block hash and transaction index.
890	pub async fn receipt_by_block_hash_and_index(
891		&self,
892		block_hash: &H256,
893		transaction_index: usize,
894	) -> Option<ReceiptInfo> {
895		let block = self.block_provider.block_by_hash(block_hash).await.ok()??;
896		let (_, receipt) = self
897			.receipt_extractor
898			.extract_from_transaction(&block, transaction_index)
899			.await
900			.ok()?;
901		Some(receipt)
902	}
903
904	/// Get the receipt for the given transaction hash.
905	pub async fn receipt_by_hash(&self, transaction_hash: &H256) -> Option<ReceiptInfo> {
906		let (block_hash, transaction_index) = self.find_transaction(transaction_hash).await?;
907
908		let block = match self.block_provider.block_by_hash(&block_hash).await {
909			Ok(Some(b)) => b,
910			Ok(None) => {
911				log::trace!(target: LOG_TARGET,
912					"receipt_by_hash: block {block_hash:?} not available from node (pruned?) for tx {transaction_hash:?}");
913				return None;
914			},
915			Err(err) => {
916				log::trace!(target: LOG_TARGET,
917					"receipt_by_hash: failed to fetch block {block_hash:?} for tx {transaction_hash:?}: {err:?}");
918				return None;
919			},
920		};
921
922		match self.receipt_extractor.extract_from_transaction(&block, transaction_index).await {
923			Ok((_, receipt)) => Some(receipt),
924			Err(err) => {
925				log::trace!(target: LOG_TARGET,
926					"receipt_by_hash: extraction failed for tx {transaction_hash:?} in block {block_hash:?}: {err:?}");
927				None
928			},
929		}
930	}
931
932	/// Get the signed transaction for the given transaction hash.
933	pub async fn signed_tx_by_hash(&self, transaction_hash: &H256) -> Option<TransactionSigned> {
934		let (block_hash, transaction_index) = self.find_transaction(transaction_hash).await?;
935
936		let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
937		let (signed_tx, _) = self
938			.receipt_extractor
939			.extract_from_transaction(&block, transaction_index)
940			.await
941			.inspect_err(|err| {
942				log::trace!(target: LOG_TARGET,
943					"signed_tx_by_hash: extraction failed for tx {transaction_hash:?} \
944					 in block {block_hash:?}: {err:?}");
945			})
946			.ok()?;
947		Some(signed_tx)
948	}
949}
950
951#[cfg(test)]
952mod tests {
953	use super::*;
954	use crate::{
955		ReceiptInfo,
956		test::{MockBlockInfo, MockBlockInfoProvider},
957	};
958	use alloy_primitives::{Address as AlloyAddress, B256};
959	use pallet_revive::evm::TransactionSigned;
960	use pretty_assertions::assert_eq;
961	use sp_core::{H160, H256};
962	use sqlx::SqlitePool;
963
964	async fn count(pool: &SqlitePool, table: &str, block_hash: Option<H256>) -> usize {
965		let count: i64 = match block_hash {
966			None => {
967				sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table}"))
968					.fetch_one(pool)
969					.await
970			},
971			Some(hash) => {
972				sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table} WHERE block_hash = ?"))
973					.bind(hash.as_ref())
974					.fetch_one(pool)
975					.await
976			},
977		}
978		.unwrap();
979
980		count as _
981	}
982
983	fn mock_provider() -> ReceiptProvider<MockBlockInfoProvider> {
984		ReceiptProvider {
985			db_ctx: DbContext::new(
986				SqlitePool::connect_lazy("sqlite::memory:").unwrap(),
987				DbContext::DEFAULT_MAX_VARIABLE_NUMBER,
988			),
989			block_provider: MockBlockInfoProvider {},
990			receipt_extractor: ReceiptExtractor::new_mock(),
991			keep_latest_n_blocks: None,
992			block_number_to_hashes: Default::default(),
993		}
994	}
995
996	/// Test resolver that handles Latest โ†’ `latest` and Earliest โ†’ 0.
997	fn mock_resolve_block_number_with_latest(
998		latest: u64,
999	) -> impl Fn(BlockNumberOrTag) -> std::future::Ready<anyhow::Result<U256>> {
1000		move |block: BlockNumberOrTag| {
1001			std::future::ready(Ok(match block {
1002				BlockNumberOrTag::Number(v) => U256::from(v),
1003				BlockNumberOrTag::Earliest => U256::zero(),
1004				// The mock only tracks `latest`, so the remaining tags resolve to it.
1005				_ => U256::from(latest),
1006			}))
1007		}
1008	}
1009
1010	impl ReceiptProvider<MockBlockInfoProvider> {
1011		fn with_db_ctx(mut self, db_ctx: DbContext) -> Self {
1012			self.db_ctx = db_ctx;
1013			self
1014		}
1015
1016		fn with_extractor(mut self, extractor: ReceiptExtractor) -> Self {
1017			self.receipt_extractor = extractor;
1018			self
1019		}
1020
1021		fn with_keep_latest(mut self, n: Option<usize>) -> Self {
1022			self.keep_latest_n_blocks = n;
1023			self
1024		}
1025	}
1026
1027	async fn setup_sqlite_provider(pool: SqlitePool) -> ReceiptProvider<MockBlockInfoProvider> {
1028		mock_provider()
1029			.with_db_ctx(DbContext::new(pool, DbContext::DEFAULT_MAX_VARIABLE_NUMBER))
1030			.with_keep_latest(Some(10))
1031	}
1032
1033	#[sqlx::test]
1034	async fn test_insert_remove(pool: SqlitePool) -> anyhow::Result<()> {
1035		let provider = setup_sqlite_provider(pool).await;
1036		let block = MockBlockInfo { hash: H256::default(), number: 0 };
1037		let receipts = vec![(
1038			TransactionSigned::default(),
1039			ReceiptInfo {
1040				logs: vec![Log { block_hash: block.hash, ..Default::default() }],
1041				..Default::default()
1042			},
1043		)];
1044		let ethereum_hash = H256::from([1_u8; 32]);
1045		let block_map = BlockHashMap::new(block.hash(), ethereum_hash);
1046
1047		provider.insert(&block, &receipts, &ethereum_hash).await?;
1048		let row = provider.find_transaction(&receipts[0].1.transaction_hash).await;
1049		assert_eq!(row, Some((block.hash, 0)));
1050		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", Some(block.hash())).await, 1);
1051		assert_eq!(count(&provider.db_ctx.pool, "logs", Some(ethereum_hash)).await, 1);
1052
1053		provider.remove(&[block_map]).await?;
1054		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", Some(block.hash())).await, 0);
1055		assert_eq!(count(&provider.db_ctx.pool, "logs", Some(ethereum_hash)).await, 0);
1056		Ok(())
1057	}
1058
1059	#[sqlx::test]
1060	async fn test_prune(pool: SqlitePool) -> anyhow::Result<()> {
1061		let provider = setup_sqlite_provider(pool).await;
1062		let n = provider.keep_latest_n_blocks.unwrap();
1063
1064		for i in 0..2 * n {
1065			let block = MockBlockInfo { hash: H256::from([i as u8; 32]), number: i as _ };
1066			let transaction_hash = H256::from([i as u8; 32]);
1067			let receipts = vec![(
1068				TransactionSigned::default(),
1069				ReceiptInfo {
1070					transaction_hash,
1071					logs: vec![Log {
1072						block_hash: block.hash,
1073						transaction_hash,
1074						..Default::default()
1075					}],
1076					..Default::default()
1077				},
1078			)];
1079			let ethereum_hash = H256::from([(i + 1) as u8; 32]);
1080			provider.insert(&block, &receipts, &ethereum_hash).await?;
1081		}
1082		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, n);
1083		assert_eq!(count(&provider.db_ctx.pool, "logs", None).await, n);
1084		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, n);
1085		assert_eq!(provider.block_number_to_hashes.lock().await.len(), n);
1086
1087		return Ok(());
1088	}
1089
1090	#[sqlx::test]
1091	async fn test_fork(pool: SqlitePool) -> anyhow::Result<()> {
1092		let provider = setup_sqlite_provider(pool).await;
1093
1094		let build_block = |seed, number| {
1095			let block = MockBlockInfo { hash: H256::from([seed; 32]), number };
1096			let transaction_hash = H256::from([seed; 32]);
1097			let receipts = vec![(
1098				TransactionSigned::default(),
1099				ReceiptInfo {
1100					transaction_hash,
1101					logs: vec![Log {
1102						block_hash: block.hash,
1103						transaction_hash,
1104						..Default::default()
1105					}],
1106					..Default::default()
1107				},
1108			)];
1109			let ethereum_hash = H256::from([seed + 1; 32]);
1110
1111			(block, receipts, ethereum_hash)
1112		};
1113
1114		// Build 4 blocks on consecutive heights: 0,1,2,3.
1115		let (block0, receipts, ethereum_hash_0) = build_block(0, 0);
1116		provider.insert(&block0, &receipts, &ethereum_hash_0).await?;
1117		let (block1, receipts, ethereum_hash_1) = build_block(1, 1);
1118		provider.insert(&block1, &receipts, &ethereum_hash_1).await?;
1119		let (block2, receipts, ethereum_hash_2) = build_block(2, 2);
1120		provider.insert(&block2, &receipts, &ethereum_hash_2).await?;
1121		let (block3, receipts, ethereum_hash_3) = build_block(3, 3);
1122		provider.insert(&block3, &receipts, &ethereum_hash_3).await?;
1123
1124		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 4);
1125		assert_eq!(count(&provider.db_ctx.pool, "logs", None).await, 4);
1126		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 4);
1127		assert_eq!(
1128			provider.block_number_to_hashes.lock().await.clone(),
1129			[
1130				(0, BlockHashMap::new(block0.hash, ethereum_hash_0)),
1131				(1, BlockHashMap::new(block1.hash, ethereum_hash_1)),
1132				(2, BlockHashMap::new(block2.hash, ethereum_hash_2)),
1133				(3, BlockHashMap::new(block3.hash, ethereum_hash_3))
1134			]
1135			.into(),
1136		);
1137
1138		// Now build another block on height 1.
1139		let (fork_block, receipts, ethereum_hash_fork) = build_block(4, 1);
1140		provider.insert(&fork_block, &receipts, &ethereum_hash_fork).await?;
1141
1142		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 2);
1143		assert_eq!(count(&provider.db_ctx.pool, "logs", None).await, 2);
1144		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 2);
1145
1146		assert_eq!(
1147			provider.block_number_to_hashes.lock().await.clone(),
1148			[
1149				(0, BlockHashMap::new(block0.hash, ethereum_hash_0)),
1150				(1, BlockHashMap::new(fork_block.hash, ethereum_hash_fork))
1151			]
1152			.into(),
1153		);
1154
1155		return Ok(());
1156	}
1157
1158	#[sqlx::test]
1159	async fn test_reorg_same_transaction_hash(pool: SqlitePool) -> anyhow::Result<()> {
1160		let provider = setup_sqlite_provider(pool).await;
1161
1162		// Build two blocks at the same height with the same transaction hash
1163		let tx_hash = H256::from([42u8; 32]);
1164
1165		// Block A at height 1
1166		let block_a = MockBlockInfo { hash: H256::from([1u8; 32]), number: 1 };
1167		let ethereum_hash_a = H256::from([2u8; 32]);
1168		let receipts_a = vec![(
1169			TransactionSigned::default(),
1170			ReceiptInfo {
1171				transaction_hash: tx_hash,
1172				transaction_index: U256::from(0),
1173				..Default::default()
1174			},
1175		)];
1176
1177		provider.insert(&block_a, &receipts_a, &ethereum_hash_a).await?;
1178
1179		// Verify transaction points to block A
1180		let (found_hash, _) = provider.find_transaction(&tx_hash).await.unwrap();
1181		assert_eq!(found_hash, block_a.hash);
1182
1183		// Clear the in-memory map to simulate server restart
1184		provider.block_number_to_hashes.lock().await.clear();
1185
1186		// Block B at same height 1 (re-org) with SAME transaction
1187		let block_b = MockBlockInfo { hash: H256::from([3u8; 32]), number: 1 };
1188		let ethereum_hash_b = H256::from([4u8; 32]);
1189		let receipts_b = vec![(
1190			TransactionSigned::default(),
1191			ReceiptInfo {
1192				transaction_hash: tx_hash, // Same tx hash!
1193				transaction_index: U256::from(0),
1194				..Default::default()
1195			},
1196		)];
1197
1198		// This should NOT fail with UNIQUE constraint violation
1199		provider.insert(&block_b, &receipts_b, &ethereum_hash_b).await?;
1200
1201		// Transaction should now point to block B
1202		let (found_hash, _) = provider.find_transaction(&tx_hash).await.unwrap();
1203		assert_eq!(found_hash, block_b.hash);
1204
1205		Ok(())
1206	}
1207
1208	#[sqlx::test]
1209	async fn test_receipts_count_per_block(pool: SqlitePool) -> anyhow::Result<()> {
1210		let provider = setup_sqlite_provider(pool).await;
1211		let block = MockBlockInfo { hash: H256::default(), number: 0 };
1212		let receipts = vec![
1213			(
1214				TransactionSigned::default(),
1215				ReceiptInfo { transaction_hash: H256::from([0u8; 32]), ..Default::default() },
1216			),
1217			(
1218				TransactionSigned::default(),
1219				ReceiptInfo { transaction_hash: H256::from([1u8; 32]), ..Default::default() },
1220			),
1221		];
1222		let ethereum_hash = H256::from([2u8; 32]);
1223
1224		provider.insert(&block, &receipts, &ethereum_hash).await?;
1225		let count = provider.receipts_count_per_block(&block.hash).await;
1226		assert_eq!(count, Some(2));
1227		Ok(())
1228	}
1229
1230	#[sqlx::test]
1231	async fn test_query_logs(pool: SqlitePool) -> anyhow::Result<()> {
1232		let provider = setup_sqlite_provider(pool).await;
1233		let block1 = MockBlockInfo { hash: H256::from([1u8; 32]), number: 1 };
1234		let block2 = MockBlockInfo { hash: H256::from([2u8; 32]), number: 2 };
1235		let ethereum_hash1 = H256::from([3u8; 32]);
1236		let ethereum_hash2 = H256::from([4u8; 32]);
1237		let log1 = Log {
1238			block_hash: ethereum_hash1,
1239			block_number: block1.number.into(),
1240			address: H160::from([1u8; 20]),
1241			topics: vec![H256::from([1u8; 32]), H256::from([2u8; 32])],
1242			data: Some(vec![0u8; 32].into()),
1243			transaction_hash: H256::default(),
1244			transaction_index: U256::from(1),
1245			log_index: U256::from(1),
1246			..Default::default()
1247		};
1248		let log2 = Log {
1249			block_hash: ethereum_hash2,
1250			block_number: block2.number.into(),
1251			address: H160::from([2u8; 20]),
1252			topics: vec![H256::from([2u8; 32]), H256::from([3u8; 32])],
1253			transaction_hash: H256::from([1u8; 32]),
1254			transaction_index: U256::from(2),
1255			log_index: U256::from(1),
1256			..Default::default()
1257		};
1258
1259		provider
1260			.insert(
1261				&block1,
1262				&vec![(
1263					TransactionSigned::default(),
1264					ReceiptInfo {
1265						logs: vec![log1.clone()],
1266						transaction_hash: log1.transaction_hash,
1267						transaction_index: log1.transaction_index,
1268						..Default::default()
1269					},
1270				)],
1271				&ethereum_hash1,
1272			)
1273			.await?;
1274		provider
1275			.insert(
1276				&block2,
1277				&vec![(
1278					TransactionSigned::default(),
1279					ReceiptInfo {
1280						logs: vec![log2.clone()],
1281						transaction_hash: log2.transaction_hash,
1282						transaction_index: log2.transaction_index,
1283						..Default::default()
1284					},
1285				)],
1286				&ethereum_hash2,
1287			)
1288			.await?;
1289
1290		let resolve_block_number = mock_resolve_block_number_with_latest(block2.number.into());
1291
1292		// Empty filter
1293		let logs = provider.logs(None, &resolve_block_number).await?;
1294		assert_eq!(logs, vec![log2.clone()]);
1295
1296		// from_block filter
1297		let logs = provider
1298			.logs(Some(Filter::new().from_block(log2.block_number.as_u64())), &resolve_block_number)
1299			.await?;
1300		assert_eq!(logs, vec![log2.clone()]);
1301
1302		// from_block filter (using latest block)
1303		let logs = provider
1304			.logs(Some(Filter::new().from_block(BlockNumberOrTag::Latest)), &resolve_block_number)
1305			.await?;
1306		assert_eq!(logs, vec![log2.clone()]);
1307
1308		// to_block filter
1309		let logs = provider
1310			.logs(Some(Filter::new().to_block(log1.block_number.as_u64())), &resolve_block_number)
1311			.await?;
1312		assert_eq!(logs, vec![log1.clone()]);
1313
1314		// block_hash filter
1315		let logs = provider
1316			.logs(
1317				Some(Filter::new().at_block_hash(B256::from(log1.block_hash.0))),
1318				&resolve_block_number,
1319			)
1320			.await?;
1321		assert_eq!(logs, vec![log1.clone()]);
1322
1323		// single address
1324		let logs = provider
1325			.logs(
1326				Some(
1327					Filter::new()
1328						.from_block(BlockNumberOrTag::Earliest)
1329						.address(AlloyAddress::from(log1.address.0)),
1330				),
1331				&resolve_block_number,
1332			)
1333			.await?;
1334		assert_eq!(logs, vec![log1.clone()]);
1335
1336		// multiple addresses
1337		let logs = provider
1338			.logs(
1339				Some(Filter::new().from_block(BlockNumberOrTag::Earliest).address(vec![
1340					AlloyAddress::from(log1.address.0),
1341					AlloyAddress::from(log2.address.0),
1342				])),
1343				&resolve_block_number,
1344			)
1345			.await?;
1346		assert_eq!(logs, vec![log1.clone(), log2.clone()]);
1347
1348		// single topic
1349		let logs = provider
1350			.logs(
1351				Some(
1352					Filter::new()
1353						.from_block(BlockNumberOrTag::Earliest)
1354						.event_signature(B256::from(log1.topics[0].0)),
1355				),
1356				&resolve_block_number,
1357			)
1358			.await?;
1359		assert_eq!(logs, vec![log1.clone()]);
1360
1361		// multiple topic
1362		let logs = provider
1363			.logs(
1364				Some(
1365					Filter::new()
1366						.from_block(BlockNumberOrTag::Earliest)
1367						.event_signature(B256::from(log1.topics[0].0))
1368						.topic1(B256::from(log1.topics[1].0)),
1369				),
1370				&resolve_block_number,
1371			)
1372			.await?;
1373		assert_eq!(logs, vec![log1.clone()]);
1374
1375		// multiple topic for topic_0
1376		let logs = provider
1377			.logs(
1378				Some(Filter::new().from_block(BlockNumberOrTag::Earliest).event_signature(vec![
1379					B256::from(log1.topics[0].0),
1380					B256::from(log2.topics[0].0),
1381				])),
1382				&resolve_block_number,
1383			)
1384			.await?;
1385		assert_eq!(logs, vec![log1.clone(), log2.clone()]);
1386
1387		// Altogether
1388		let logs = provider
1389			.logs(
1390				Some(
1391					Filter::new()
1392						.from_block(BlockNumberOrTag::Earliest)
1393						.to_block(BlockNumberOrTag::Latest)
1394						.address(vec![
1395							AlloyAddress::from(log1.address.0),
1396							AlloyAddress::from(log2.address.0),
1397						])
1398						.event_signature(vec![
1399							B256::from(log1.topics[0].0),
1400							B256::from(log2.topics[0].0),
1401						]),
1402				),
1403				&resolve_block_number,
1404			)
1405			.await?;
1406		assert_eq!(logs, vec![log1.clone(), log2.clone()]);
1407
1408		for tag in [
1409			BlockNumberOrTag::Latest,
1410			BlockNumberOrTag::Finalized,
1411			BlockNumberOrTag::Safe,
1412			BlockNumberOrTag::Pending,
1413		] {
1414			let logs = provider
1415				.logs(Some(Filter::new().from_block(tag)), &resolve_block_number)
1416				.await?;
1417			assert_eq!(logs, vec![log2.clone()], "tag {tag:?} should resolve to the latest block");
1418		}
1419
1420		let logs = provider
1421			.logs(
1422				Some(
1423					Filter::new()
1424						.from_block(log1.block_number.as_u64())
1425						.to_block(log1.block_number.as_u64()),
1426				),
1427				&resolve_block_number,
1428			)
1429			.await?;
1430		assert_eq!(logs, vec![log1.clone()], "from == to selects the single block");
1431
1432		let result = provider
1433			.logs(
1434				Some(
1435					Filter::new()
1436						.from_block(log2.block_number.as_u64())
1437						.to_block(log1.block_number.as_u64()),
1438				),
1439				&resolve_block_number,
1440			)
1441			.await;
1442		assert!(result.is_err(), "from > to should be rejected");
1443
1444		let result = provider
1445			.logs(
1446				Some(Filter::new().from_block(log2.block_number.as_u64() + 100)),
1447				&resolve_block_number,
1448			)
1449			.await;
1450		assert!(result.is_err(), "block number beyond latest should be rejected");
1451
1452		let failing = |_: BlockNumberOrTag| {
1453			std::future::ready(Err::<U256, anyhow::Error>(anyhow::anyhow!("resolver failed")))
1454		};
1455		let result = provider
1456			.logs(Some(Filter::new().from_block(BlockNumberOrTag::Latest)), &failing)
1457			.await;
1458		assert!(result.is_err(), "a resolver error should propagate");
1459
1460		Ok(())
1461	}
1462
1463	#[sqlx::test]
1464	async fn test_block_mapping_insert_get(pool: SqlitePool) -> anyhow::Result<()> {
1465		let provider = setup_sqlite_provider(pool).await;
1466		let ethereum_hash = H256::from([1u8; 32]);
1467		let substrate_hash = H256::from([2u8; 32]);
1468		let block_map = BlockHashMap::new(substrate_hash, ethereum_hash);
1469
1470		// Insert mapping
1471		insert_block_mapping(&provider.db_ctx.pool, &block_map).await?;
1472
1473		// Test forward lookup
1474		let resolved = provider.get_substrate_hash(&ethereum_hash).await;
1475		assert_eq!(resolved, Some(substrate_hash));
1476
1477		// Test reverse lookup
1478		let resolved = provider.get_ethereum_hash(&substrate_hash).await;
1479		assert_eq!(resolved, Some(ethereum_hash));
1480
1481		Ok(())
1482	}
1483
1484	#[sqlx::test]
1485	async fn test_block_mapping_remove(pool: SqlitePool) -> anyhow::Result<()> {
1486		let provider = setup_sqlite_provider(pool).await;
1487		let ethereum_hash1 = H256::from([1u8; 32]);
1488		let ethereum_hash2 = H256::from([2u8; 32]);
1489		let substrate_hash1 = H256::from([3u8; 32]);
1490		let substrate_hash2 = H256::from([4u8; 32]);
1491		let block_map1 = BlockHashMap::new(substrate_hash1, ethereum_hash1);
1492		let block_map2 = BlockHashMap::new(substrate_hash2, ethereum_hash2);
1493
1494		// Insert mappings
1495		insert_block_mapping(&provider.db_ctx.pool, &block_map1).await?;
1496		insert_block_mapping(&provider.db_ctx.pool, &block_map2).await?;
1497
1498		// Verify they exist
1499		assert_eq!(
1500			provider.get_substrate_hash(&block_map1.ethereum_hash).await,
1501			Some(block_map1.substrate_hash)
1502		);
1503		assert_eq!(
1504			provider.get_substrate_hash(&block_map2.ethereum_hash).await,
1505			Some(block_map2.substrate_hash)
1506		);
1507
1508		// Remove one mapping
1509		provider.remove(&[block_map1]).await?;
1510
1511		// Verify removal
1512		assert_eq!(provider.get_substrate_hash(&ethereum_hash1).await, None);
1513		assert_eq!(provider.get_substrate_hash(&ethereum_hash2).await, Some(substrate_hash2));
1514
1515		Ok(())
1516	}
1517
1518	#[sqlx::test]
1519	async fn test_block_mapping_pruning_integration(pool: SqlitePool) -> anyhow::Result<()> {
1520		let provider = setup_sqlite_provider(pool).await;
1521		let ethereum_hash = H256::from([1u8; 32]);
1522		let substrate_hash = H256::from([2u8; 32]);
1523		let block_map = BlockHashMap::new(substrate_hash, ethereum_hash);
1524
1525		// Insert mapping
1526		insert_block_mapping(&provider.db_ctx.pool, &block_map).await?;
1527		assert_eq!(
1528			provider.get_substrate_hash(&block_map.ethereum_hash).await,
1529			Some(block_map.substrate_hash)
1530		);
1531
1532		// Remove substrate block (this should also remove the mapping)
1533		provider.remove(&[block_map.clone()]).await?;
1534
1535		// Mapping should be gone
1536		assert_eq!(provider.get_substrate_hash(&block_map.ethereum_hash).await, None);
1537
1538		Ok(())
1539	}
1540
1541	#[sqlx::test]
1542	async fn test_logs_with_ethereum_block_hash_mapping(pool: SqlitePool) -> anyhow::Result<()> {
1543		let provider = setup_sqlite_provider(pool).await;
1544		let ethereum_hash = H256::from([1u8; 32]);
1545		let substrate_hash = H256::from([2u8; 32]);
1546		let block_number = 1u64;
1547
1548		// Create a log with ethereum hash
1549		let log = Log {
1550			block_hash: ethereum_hash,
1551			block_number: block_number.into(),
1552			address: H160::from([1u8; 20]),
1553			topics: vec![H256::from([1u8; 32])],
1554			transaction_hash: H256::from([3u8; 32]),
1555			transaction_index: U256::from(0),
1556			log_index: U256::from(0),
1557			data: Some(vec![0u8; 32].into()),
1558			..Default::default()
1559		};
1560
1561		// Insert the log
1562		let block = MockBlockInfo { hash: substrate_hash, number: block_number };
1563		let receipts = vec![(
1564			TransactionSigned::default(),
1565			ReceiptInfo {
1566				logs: vec![log.clone()],
1567				transaction_hash: log.transaction_hash,
1568				transaction_index: log.transaction_index,
1569				..Default::default()
1570			},
1571		)];
1572		provider.insert(&block, &receipts, &ethereum_hash).await?;
1573
1574		// Query logs using Ethereum block hash (should resolve to substrate hash)
1575		let logs = provider
1576			.logs(
1577				Some(Filter::new().at_block_hash(B256::from(ethereum_hash.0))),
1578				mock_resolve_block_number_with_latest(block.number.into()),
1579			)
1580			.await?;
1581		assert_eq!(logs, vec![log]);
1582
1583		Ok(())
1584	}
1585
1586	#[sqlx::test]
1587	async fn test_mapping_count(pool: SqlitePool) -> anyhow::Result<()> {
1588		let provider = setup_sqlite_provider(pool).await;
1589
1590		// Initially no mappings
1591		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 0);
1592
1593		let block_map1 = BlockHashMap::new(H256::from([1u8; 32]), H256::from([2u8; 32]));
1594		let block_map2 = BlockHashMap::new(H256::from([3u8; 32]), H256::from([4u8; 32]));
1595
1596		// Insert some mappings
1597		insert_block_mapping(&provider.db_ctx.pool, &block_map1).await?;
1598		insert_block_mapping(&provider.db_ctx.pool, &block_map2).await?;
1599
1600		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 2);
1601
1602		// Remove one
1603		provider.remove(&[block_map1]).await?;
1604		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 1);
1605
1606		Ok(())
1607	}
1608
1609	#[sqlx::test]
1610	async fn restore_first_evm_block_clears_stale(pool: SqlitePool) -> anyhow::Result<()> {
1611		let provider = setup_sqlite_provider(pool).await;
1612
1613		// Persist first_evm_block = 42.
1614		provider
1615			.set_sync_label(ChainMetadata::FirstEvmBlock, SyncCheckpoint::from_number(42))
1616			.await?;
1617
1618		// MockBlockInfoProvider returns no blocks, so has_evm_hash is always false.
1619		// This means evm_first=42 is stale (no longer has an EVM hash).
1620		provider.restore_first_evm_block().await?;
1621
1622		// The value should have been cleared (not restored to the extractor).
1623		assert_eq!(provider.first_evm_block(), None);
1624
1625		// DB row should have been deleted.
1626		let checkpoint = provider.get_sync_label(ChainMetadata::FirstEvmBlock).await?;
1627		assert!(checkpoint.is_none());
1628		Ok(())
1629	}
1630
1631	#[sqlx::test]
1632	async fn advance_sync_label_only_increases(pool: SqlitePool) -> anyhow::Result<()> {
1633		let provider = setup_sqlite_provider(pool).await;
1634		let hash_a = H256::repeat_byte(0xAA);
1635		let hash_b = H256::repeat_byte(0xBB);
1636
1637		// First insert creates the row.
1638		provider
1639			.advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(100, hash_a))
1640			.await?;
1641		let checkpoint = provider.get_sync_label(SyncLabel::Head).await?.unwrap();
1642		assert_eq!((checkpoint.block_number, checkpoint.block_hash), (100, Some(hash_a)));
1643
1644		// Higher value advances.
1645		provider
1646			.advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(200, hash_b))
1647			.await?;
1648		let checkpoint = provider.get_sync_label(SyncLabel::Head).await?.unwrap();
1649		assert_eq!((checkpoint.block_number, checkpoint.block_hash), (200, Some(hash_b)));
1650
1651		// Lower and equal values are ignored (strict >).
1652		provider
1653			.advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(50, hash_a))
1654			.await?;
1655		provider
1656			.advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(200, hash_a))
1657			.await?;
1658		let checkpoint = provider.get_sync_label(SyncLabel::Head).await?.unwrap();
1659		assert_eq!((checkpoint.block_number, checkpoint.block_hash), (200, Some(hash_b)));
1660
1661		Ok(())
1662	}
1663
1664	#[sqlx::test]
1665	async fn recede_sync_label_only_decreases(pool: SqlitePool) -> anyhow::Result<()> {
1666		let provider = setup_sqlite_provider(pool).await;
1667		let hash_a = H256::repeat_byte(0xAA);
1668		let hash_b = H256::repeat_byte(0xBB);
1669
1670		// First insert creates the row.
1671		provider
1672			.recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(100, hash_a))
1673			.await?;
1674		let checkpoint = provider.get_sync_label(SyncLabel::Tail).await?.unwrap();
1675		assert_eq!((checkpoint.block_number, checkpoint.block_hash), (100, Some(hash_a)));
1676
1677		// Lower value recedes.
1678		provider
1679			.recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(50, hash_b))
1680			.await?;
1681		let checkpoint = provider.get_sync_label(SyncLabel::Tail).await?.unwrap();
1682		assert_eq!((checkpoint.block_number, checkpoint.block_hash), (50, Some(hash_b)));
1683
1684		// Higher and equal values are ignored (strict <).
1685		provider
1686			.recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(200, hash_a))
1687			.await?;
1688		provider
1689			.recede_sync_label(SyncLabel::Tail, SyncCheckpoint::new(50, hash_a))
1690			.await?;
1691		let checkpoint = provider.get_sync_label(SyncLabel::Tail).await?.unwrap();
1692		assert_eq!((checkpoint.block_number, checkpoint.block_hash), (50, Some(hash_b)));
1693
1694		Ok(())
1695	}
1696
1697	#[tokio::test]
1698	async fn is_before_earliest_block_edge_cases() {
1699		// U256 > u32::MAX should never be considered "before floor"
1700		let extractor = ReceiptExtractor::new_mock();
1701		extractor.set_first_evm_block(10);
1702		let provider = mock_provider().with_extractor(extractor);
1703
1704		let huge = BlockNumberOrTag::Number(u64::MAX);
1705		assert!(!provider.is_before_earliest_block(&huge));
1706
1707		let just_over = BlockNumberOrTag::Number(u32::MAX as u64 + 1);
1708		assert!(!provider.is_before_earliest_block(&just_over));
1709
1710		// Sentinel first_evm_block (u32::MAX) is permissive โ€” no queries rejected.
1711		let provider = mock_provider();
1712		assert!(!provider.is_before_earliest_block(&BlockNumberOrTag::Number(0)));
1713		assert!(!provider.is_before_earliest_block(&BlockNumberOrTag::Number(1_000_000)));
1714
1715		// Tag-based queries are never rejected.
1716		assert!(!provider.is_before_earliest_block(&BlockNumberOrTag::Latest));
1717	}
1718
1719	#[sqlx::test]
1720	async fn persistent_mode_caps_in_memory_map(pool: SqlitePool) -> anyhow::Result<()> {
1721		// Persistent DB mode: keep_latest_n_blocks = None
1722		let provider = mock_provider()
1723			.with_db_ctx(DbContext::new(pool, DbContext::DEFAULT_MAX_VARIABLE_NUMBER));
1724
1725		// Insert more than MAX_CACHED_BLOCKS blocks.
1726		let start_block: u64 = 1;
1727		let n = MAX_CACHED_BLOCKS + 1;
1728		let end_block = start_block + n as u64;
1729		for i in start_block..end_block {
1730			let block = MockBlockInfo { hash: H256::from_low_u64_be(i), number: i as _ };
1731			let receipts = vec![(
1732				TransactionSigned::default(),
1733				ReceiptInfo {
1734					transaction_hash: H256::from_low_u64_be(i),
1735					logs: vec![Log {
1736						block_hash: block.hash,
1737						transaction_hash: H256::from_low_u64_be(i),
1738						..Default::default()
1739					}],
1740					..Default::default()
1741				},
1742			)];
1743			let ethereum_hash = H256::from_low_u64_be(i + 1);
1744			provider.insert(&block, &receipts, &ethereum_hash).await?;
1745		}
1746
1747		// The map is capped at MAX_CACHED_BLOCKS.
1748		let map = provider.block_number_to_hashes.lock().await;
1749		assert_eq!(map.len(), MAX_CACHED_BLOCKS);
1750
1751		// The oldest block (1) should have been evicted, keeping blocks 2..=MAX+1.
1752		assert!(!map.contains_key(&1));
1753		assert!(map.contains_key(&2));
1754		assert!(map.contains_key(&(MAX_CACHED_BLOCKS as u64 + 1)));
1755		drop(map);
1756
1757		// All blocks are still in the DB.
1758		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, n);
1759
1760		Ok(())
1761	}
1762
1763	fn make_hash(i: usize, fill: u8) -> H256 {
1764		let mut hash = [fill; 32];
1765		hash[..8].copy_from_slice(&i.to_le_bytes());
1766		H256::from(hash)
1767	}
1768
1769	fn make_receipts(
1770		tx_offset: usize,
1771		n_tx: usize,
1772		n_logs: usize,
1773	) -> Vec<(TransactionSigned, ReceiptInfo)> {
1774		let mut receipts = Vec::with_capacity(n_tx);
1775
1776		for i in 0..n_tx {
1777			let transaction_hash = make_hash(tx_offset + i, 0x00);
1778
1779			let mut logs = Vec::with_capacity(n_logs);
1780			for j in 0..n_logs {
1781				logs.push(Log { transaction_hash, log_index: U256::from(j), ..Default::default() });
1782			}
1783
1784			receipts.push((
1785				TransactionSigned::default(),
1786				ReceiptInfo {
1787					transaction_hash,
1788					transaction_index: U256::from(i),
1789					logs,
1790					..Default::default()
1791				},
1792			));
1793		}
1794
1795		receipts
1796	}
1797
1798	async fn assert_receipts_inserted(
1799		provider: &ReceiptProvider<MockBlockInfoProvider>,
1800		block: &MockBlockInfo,
1801		ethereum_hash: &H256,
1802		receipts: &[(TransactionSigned, ReceiptInfo)],
1803	) {
1804		let mut expected_logs = 0;
1805		for (_, receipt) in receipts {
1806			assert_eq!(
1807				provider.find_transaction(&receipt.transaction_hash).await,
1808				Some((block.hash(), receipt.transaction_index.as_u32() as usize))
1809			);
1810			expected_logs += receipt.logs.len();
1811		}
1812		assert_eq!(count(&provider.db_ctx.pool, "logs", Some(*ethereum_hash)).await, expected_logs);
1813	}
1814
1815	#[sqlx::test]
1816	async fn test_bulk_insert(pool: SqlitePool) -> anyhow::Result<()> {
1817		let provider = setup_sqlite_provider(pool).await.with_keep_latest(None);
1818		let tx_chunk = provider.db_ctx.tx_insert_chunk_size;
1819		let log_chunk = provider.db_ctx.log_insert_chunk_size;
1820
1821		let cases = [
1822			(tx_chunk, 1),             // exact tx chunk boundary
1823			(tx_chunk + 1, log_chunk), // crosses tx boundary; exact log chunk boundary
1824			(1000, 3),                 // multiple tx and log chunks
1825		];
1826
1827		let mut tx_offset = 0;
1828		for (i, (n_tx, n_logs)) in cases.into_iter().enumerate() {
1829			let block = MockBlockInfo { hash: make_hash(i, 0x00), number: i as u64 + 1 };
1830			let ethereum_hash = make_hash(i, 0xff);
1831			let receipts = make_receipts(tx_offset, n_tx, n_logs);
1832			tx_offset += n_tx;
1833			provider.insert(&block, &receipts, &ethereum_hash).await?;
1834			assert_receipts_inserted(&provider, &block, &ethereum_hash, &receipts).await;
1835		}
1836		Ok(())
1837	}
1838
1839	#[sqlx::test]
1840	async fn test_duplicate_insert_succeeds(pool: SqlitePool) -> anyhow::Result<()> {
1841		let provider = setup_sqlite_provider(pool).await.with_keep_latest(None);
1842		let block = MockBlockInfo { hash: make_hash(0, 0xAA), number: 1 };
1843		let ethereum_hash = make_hash(0, 0xBB);
1844		let receipts = make_receipts(0, 5, 3);
1845
1846		// First insert.
1847		provider.insert_into_db(&block, &receipts, &ethereum_hash).await?;
1848		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 5);
1849		assert_eq!(count(&provider.db_ctx.pool, "logs", Some(ethereum_hash)).await, 15);
1850		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 1);
1851
1852		// Delete only the block mapping so the EXISTS guard won't short-circuit.
1853		sqlx::query("DELETE FROM eth_to_substrate_blocks")
1854			.execute(&provider.db_ctx.pool)
1855			.await?;
1856		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 0);
1857
1858		// Second insert hits the actual INSERT OR REPLACE statements.
1859		provider.insert_into_db(&block, &receipts, &ethereum_hash).await?;
1860
1861		// Row counts unchanged โ€” no duplicates.
1862		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 5);
1863		assert_eq!(count(&provider.db_ctx.pool, "logs", Some(ethereum_hash)).await, 15);
1864		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 1);
1865
1866		Ok(())
1867	}
1868
1869	#[sqlx::test]
1870	async fn test_insert_empty_receipts(pool: SqlitePool) -> anyhow::Result<()> {
1871		let provider = setup_sqlite_provider(pool).await.with_keep_latest(None);
1872		let block = MockBlockInfo { hash: H256::from([1u8; 32]), number: 1 };
1873		let ethereum_hash = H256::from([2u8; 32]);
1874
1875		provider.insert(&block, &[], &ethereum_hash).await?;
1876
1877		// Block mapping is stored as a deduplication marker.
1878		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 1);
1879		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 0);
1880		assert_eq!(count(&provider.db_ctx.pool, "logs", None).await, 0);
1881
1882		// Second insert for the same block is a no-op, even with receipts.
1883		let receipts = make_receipts(0, 3, 2);
1884		provider.insert(&block, &receipts, &ethereum_hash).await?;
1885		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 1);
1886		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 0);
1887		assert_eq!(count(&provider.db_ctx.pool, "logs", None).await, 0);
1888
1889		Ok(())
1890	}
1891
1892	#[sqlx::test]
1893	async fn test_bulk_delete(pool: SqlitePool) -> anyhow::Result<()> {
1894		// Use the smallest valid limit to force chunked INSERTs and DELETEs.
1895		let db_ctx = DbContext::new(pool, DbContext::LOG_COLUMNS);
1896		let provider = mock_provider().with_db_ctx(db_ctx).with_keep_latest(None);
1897
1898		let n_blocks = 25;
1899		let n_tx_per_block = 5;
1900		let n_logs_per_receipt = 3;
1901		let mut block_mappings = Vec::new();
1902
1903		for i in 0..n_blocks {
1904			let block = MockBlockInfo { hash: make_hash(i, 0xAA), number: i as u64 + 1 };
1905			let ethereum_hash = make_hash(i, 0xBB);
1906			let receipts = make_receipts(i * n_tx_per_block, n_tx_per_block, n_logs_per_receipt);
1907			provider.insert_into_db(&block, &receipts, &ethereum_hash).await?;
1908			block_mappings.push(BlockHashMap::new(block.hash, ethereum_hash));
1909		}
1910
1911		assert_eq!(
1912			count(&provider.db_ctx.pool, "transaction_hashes", None).await,
1913			n_blocks * n_tx_per_block
1914		);
1915		assert_eq!(
1916			count(&provider.db_ctx.pool, "logs", None).await,
1917			n_blocks * n_tx_per_block * n_logs_per_receipt
1918		);
1919		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, n_blocks);
1920
1921		provider.remove(&block_mappings).await?;
1922
1923		assert_eq!(count(&provider.db_ctx.pool, "transaction_hashes", None).await, 0);
1924		assert_eq!(count(&provider.db_ctx.pool, "logs", None).await, 0);
1925		assert_eq!(count(&provider.db_ctx.pool, "eth_to_substrate_blocks", None).await, 0);
1926
1927		Ok(())
1928	}
1929
1930	#[sqlx::test]
1931	async fn test_get_processed_eth_block_hash(pool: SqlitePool) -> anyhow::Result<()> {
1932		let provider = setup_sqlite_provider(pool).await;
1933		let block = MockBlockInfo { hash: H256::from([0xAA; 32]), number: 10 };
1934		let ethereum_hash = H256::from([0xBB; 32]);
1935		let receipts = vec![(TransactionSigned::default(), ReceiptInfo::default())];
1936
1937		// Not cached yet
1938		assert!(provider.get_processed_eth_block_hash(10, block.hash).await.is_none());
1939
1940		// Insert also populates the in-memory cache
1941		provider.insert(&block, &receipts, &ethereum_hash).await?;
1942		assert_eq!(
1943			provider.get_processed_eth_block_hash(10, block.hash).await,
1944			Some(ethereum_hash)
1945		);
1946
1947		// Wrong hash for same block number
1948		assert!(
1949			provider
1950				.get_processed_eth_block_hash(10, H256::from([0xCC; 32]))
1951				.await
1952				.is_none()
1953		);
1954
1955		// Wrong block number
1956		assert!(provider.get_processed_eth_block_hash(11, block.hash).await.is_none());
1957
1958		Ok(())
1959	}
1960
1961	#[sqlx::test]
1962	async fn test_logs_by_block_number(pool: SqlitePool) -> anyhow::Result<()> {
1963		let provider = setup_sqlite_provider(pool).await;
1964		let substrate_hash = H256::from([0xAA; 32]);
1965		let tx_hash = H256::from([0xBB; 32]);
1966		let block = MockBlockInfo { hash: substrate_hash, number: 42 };
1967		let ethereum_hash = H256::from([0xCC; 32]);
1968
1969		let log0 = Log {
1970			block_hash: ethereum_hash,
1971			block_number: U256::from(42),
1972			transaction_hash: tx_hash,
1973			log_index: U256::from(0),
1974			address: H160::from([0x01; 20]),
1975			..Default::default()
1976		};
1977		let log1 = Log {
1978			block_hash: ethereum_hash,
1979			block_number: U256::from(42),
1980			transaction_hash: tx_hash,
1981			log_index: U256::from(1),
1982			address: H160::from([0x02; 20]),
1983			..Default::default()
1984		};
1985
1986		let receipts = vec![(
1987			TransactionSigned::default(),
1988			ReceiptInfo {
1989				transaction_hash: tx_hash,
1990				block_hash: ethereum_hash,
1991				logs: vec![log0.clone(), log1.clone()],
1992				..Default::default()
1993			},
1994		)];
1995
1996		// No logs before insert
1997		let logs = provider.logs_by_block_number(42, ethereum_hash).await?;
1998		assert!(logs.is_empty());
1999
2000		provider.insert(&block, &receipts, &ethereum_hash).await?;
2001
2002		// Logs returned in log_index order
2003		let logs = provider.logs_by_block_number(42, ethereum_hash).await?;
2004		assert_eq!(logs.len(), 2);
2005		assert_eq!(logs[0].address, log0.address);
2006		assert_eq!(logs[1].address, log1.address);
2007		assert_eq!(logs[0].log_index, U256::from(0));
2008		assert_eq!(logs[1].log_index, U256::from(1));
2009
2010		// Different block number returns empty
2011		let logs = provider.logs_by_block_number(43, ethereum_hash).await?;
2012		assert!(logs.is_empty());
2013
2014		// Wrong ethereum hash returns empty
2015		let logs = provider.logs_by_block_number(42, H256::from([0xDD; 32])).await?;
2016		assert!(logs.is_empty());
2017
2018		Ok(())
2019	}
2020
2021	// A block-range filter walks the (block_number, address, topics) index, which without the
2022	// ORDER BY hands each block's logs back in address order.
2023	#[sqlx::test]
2024	async fn filtered_logs_come_back_in_block_and_log_index_order(
2025		pool: SqlitePool,
2026	) -> anyhow::Result<()> {
2027		let provider = setup_sqlite_provider(pool).await;
2028		let tx_hash = H256::from([0xBB; 32]);
2029
2030		let log = |block_number: u64, log_index: u64, address: u8| Log {
2031			block_hash: H256::from([block_number as u8; 32]),
2032			block_number: U256::from(block_number),
2033			transaction_hash: tx_hash,
2034			log_index: U256::from(log_index),
2035			address: H160::from([address; 20]),
2036			..Default::default()
2037		};
2038		let insert = |logs: Vec<Log>| async {
2039			let block_number = logs[0].block_number.as_u64();
2040			let ethereum_hash = logs[0].block_hash;
2041			let block = MockBlockInfo { hash: ethereum_hash, number: block_number };
2042			let receipts = vec![(
2043				TransactionSigned::default(),
2044				ReceiptInfo {
2045					transaction_hash: tx_hash,
2046					block_hash: ethereum_hash,
2047					logs,
2048					..Default::default()
2049				},
2050			)];
2051			provider.insert(&block, &receipts, &ethereum_hash).await
2052		};
2053		// The later block goes in first and holds the lowest address.
2054		insert(vec![log(2, 0, 0x00)]).await?;
2055		insert(vec![log(1, 0, 0x02), log(1, 1, 0x01)]).await?;
2056
2057		let resolve = mock_resolve_block_number_with_latest(2);
2058		let found = provider.logs(Some(Filter::new().from_block(1).to_block(2)), &resolve).await?;
2059		assert_eq!(found, vec![log(1, 0, 0x02), log(1, 1, 0x01), log(2, 0, 0x00)]);
2060
2061		Ok(())
2062	}
2063}