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