Files
pezkuwi-sdk/bizinikiwi/pezframe/revive/rpc/src/receipt_provider.rs
T

1088 lines
34 KiB
Rust

// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{
client::{BizinikiwiBlock, BizinikiwiBlockNumber},
Address, AddressOrAddresses, BlockInfoProvider, BlockNumberOrTag, BlockTag, Bytes, ClientError,
FilterTopic, ReceiptExtractor, SubxtBlockInfoProvider,
};
use pezpallet_revive::evm::{Filter, Log, ReceiptInfo, TransactionSigned};
use pezsp_core::{H256, U256};
use sqlx::{query, QueryBuilder, Row, Sqlite, SqlitePool};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use tokio::sync::Mutex;
const LOG_TARGET: &str = "eth-rpc::receipt_provider";
/// ReceiptProvider stores transaction receipts and logs in a SQLite database.
#[derive(Clone)]
pub struct ReceiptProvider<B: BlockInfoProvider = SubxtBlockInfoProvider> {
/// The database pool.
pool: SqlitePool,
/// The block provider used to fetch blocks, and reconstruct receipts.
block_provider: B,
/// A means to extract receipts from extrinsics.
receipt_extractor: ReceiptExtractor,
/// When `Some`, old blocks will be pruned.
keep_latest_n_blocks: Option<usize>,
/// A Map of the latest block numbers to block hashes.
block_number_to_hashes: Arc<Mutex<BTreeMap<BizinikiwiBlockNumber, BlockHashMap>>>,
}
/// Bizinikiwi block to Ethereum block mapping
#[derive(Clone, Debug, PartialEq, Eq)]
struct BlockHashMap {
bizinikiwi_hash: H256,
ethereum_hash: H256,
}
impl BlockHashMap {
fn new(bizinikiwi_hash: H256, ethereum_hash: H256) -> Self {
Self { bizinikiwi_hash, ethereum_hash }
}
}
/// Provides information about a block,
/// This is an abstratction on top of [`BizinikiwiBlock`] that can't be mocked in tests.
/// Can be removed once <https://github.com/pezkuwichain/kurdistan-sdk/issues/188> is fixed.
pub trait BlockInfo {
/// Returns the block hash.
fn hash(&self) -> H256;
/// Returns the block number.
fn number(&self) -> BizinikiwiBlockNumber;
}
impl BlockInfo for BizinikiwiBlock {
fn hash(&self) -> H256 {
BizinikiwiBlock::hash(self)
}
fn number(&self) -> BizinikiwiBlockNumber {
BizinikiwiBlock::number(self)
}
}
impl<B: BlockInfoProvider> ReceiptProvider<B> {
/// Create a new `ReceiptProvider` with the given database URL and block provider.
pub async fn new(
pool: SqlitePool,
block_provider: B,
receipt_extractor: ReceiptExtractor,
keep_latest_n_blocks: Option<usize>,
) -> Result<Self, sqlx::Error> {
sqlx::migrate!().run(&pool).await?;
Ok(Self {
pool,
block_provider,
receipt_extractor,
keep_latest_n_blocks,
block_number_to_hashes: Default::default(),
})
}
// Get block hash and transaction index by transaction hash
pub async fn find_transaction(&self, transaction_hash: &H256) -> Option<(H256, usize)> {
let transaction_hash = transaction_hash.as_ref();
let result = query!(
r#"
SELECT block_hash, transaction_index
FROM transaction_hashes
WHERE transaction_hash = $1
"#,
transaction_hash
)
.fetch_optional(&self.pool)
.await
.ok()??;
let block_hash = H256::from_slice(&result.block_hash[..]);
let transaction_index = result.transaction_index.try_into().ok()?;
Some((block_hash, transaction_index))
}
/// Insert a block mapping from Ethereum block hash to Bizinikiwi block hash.
async fn insert_block_mapping(&self, block_map: &BlockHashMap) -> Result<(), ClientError> {
let ethereum_hash_ref = block_map.ethereum_hash.as_ref();
let bizinikiwi_hash_ref = block_map.bizinikiwi_hash.as_ref();
query!(
r#"
INSERT OR REPLACE INTO eth_to_bizinikiwi_blocks (ethereum_block_hash, bizinikiwi_block_hash)
VALUES ($1, $2)
"#,
ethereum_hash_ref,
bizinikiwi_hash_ref,
)
.execute(&self.pool)
.await?;
log::trace!(target: LOG_TARGET, "Insert block mapping ethereum block: {:?} -> bizinikiwi block: {:?}", block_map.ethereum_hash, block_map.bizinikiwi_hash);
Ok(())
}
/// Get the Bizinikiwi block hash for the given Ethereum block hash.
pub async fn get_bizinikiwi_hash(&self, ethereum_block_hash: &H256) -> Option<H256> {
let ethereum_hash = ethereum_block_hash.as_ref();
let result = query!(
r#"
SELECT bizinikiwi_block_hash
FROM eth_to_bizinikiwi_blocks
WHERE ethereum_block_hash = $1
"#,
ethereum_hash
)
.fetch_optional(&self.pool)
.await
.inspect_err(|e| {
log::error!(target: LOG_TARGET, "failed to get block mapping for ethereum block {ethereum_block_hash:?}, err: {e:?}");
})
.ok()?
.or_else(||{
log::trace!(target: LOG_TARGET, "No block mapping found for ethereum block: {ethereum_block_hash:?}");
None
})?;
log::trace!(target: LOG_TARGET, "Get block mapping ethereum block: {:?} -> bizinikiwi block: {ethereum_block_hash:?}", H256::from_slice(&result.bizinikiwi_block_hash[..]));
Some(H256::from_slice(&result.bizinikiwi_block_hash[..]))
}
/// Get the Ethereum block hash for the given Bizinikiwi block hash.
pub async fn get_ethereum_hash(&self, bizinikiwi_block_hash: &H256) -> Option<H256> {
let bizinikiwi_hash = bizinikiwi_block_hash.as_ref();
let result = query!(
r#"
SELECT ethereum_block_hash
FROM eth_to_bizinikiwi_blocks
WHERE bizinikiwi_block_hash = $1
"#,
bizinikiwi_hash
)
.fetch_optional(&self.pool)
.await
.inspect_err(|e| {
log::error!(target: LOG_TARGET, "failed to get block mapping for bizinikiwi block {bizinikiwi_block_hash:?}, err: {e:?}");
})
.ok()?
.or_else(||{
log::trace!(target: LOG_TARGET, "No block mapping found for bizinikiwi block: {bizinikiwi_block_hash:?}");
None
})?;
log::trace!(target: LOG_TARGET, "Get block mapping bizinikiwi block: {bizinikiwi_block_hash:?} -> ethereum block: {:?}", H256::from_slice(&result.ethereum_block_hash[..]));
Some(H256::from_slice(&result.ethereum_block_hash[..]))
}
/// Deletes older records from the database.
async fn remove(&self, block_mappings: &[BlockHashMap]) -> Result<(), ClientError> {
if block_mappings.is_empty() {
return Ok(());
}
log::debug!(target: LOG_TARGET, "Removing block hashes: {block_mappings:?}");
let placeholders = vec!["?"; block_mappings.len()].join(", ");
let sql = format!("DELETE FROM transaction_hashes WHERE block_hash in ({placeholders})");
let mut delete_tx_query = sqlx::query(&sql);
let sql = format!(
"DELETE FROM eth_to_bizinikiwi_blocks WHERE bizinikiwi_block_hash in ({placeholders})"
);
let mut delete_mappings_query = sqlx::query(&sql);
let sql = format!("DELETE FROM logs WHERE block_hash in ({placeholders})");
let mut delete_logs_query = sqlx::query(&sql);
for block_map in block_mappings {
delete_tx_query = delete_tx_query.bind(block_map.bizinikiwi_hash.as_ref());
delete_mappings_query = delete_mappings_query.bind(block_map.bizinikiwi_hash.as_ref());
// logs table uses ethereum block hash
delete_logs_query = delete_logs_query.bind(block_map.ethereum_hash.as_ref());
}
let delete_transaction_hashes = delete_tx_query.execute(&self.pool);
let delete_logs = delete_logs_query.execute(&self.pool);
let delete_mappings = delete_mappings_query.execute(&self.pool);
tokio::try_join!(delete_transaction_hashes, delete_logs, delete_mappings)?;
Ok(())
}
/// Check if the block is before the earliest block.
pub fn is_before_earliest_block(&self, at: &BlockNumberOrTag) -> bool {
match at {
BlockNumberOrTag::U256(block_number) =>
self.receipt_extractor.is_before_earliest_block(block_number.as_u32()),
BlockNumberOrTag::BlockTag(_) => false,
}
}
/// Fetch receipts from the given block.
pub async fn receipts_from_block(
&self,
block: &BizinikiwiBlock,
) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
self.receipt_extractor.extract_from_block(block).await
}
/// Extract and insert receipts from the given block.
pub async fn insert_block_receipts(
&self,
block: &BizinikiwiBlock,
ethereum_hash: &H256,
) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
let receipts = self.receipts_from_block(block).await?;
self.insert(block, &receipts, ethereum_hash).await?;
Ok(receipts)
}
/// Prune blocks older blocks.
async fn prune_blocks(
&self,
block_number: BizinikiwiBlockNumber,
block_map: &BlockHashMap,
) -> Result<(), ClientError> {
let mut to_remove = Vec::new();
let mut block_number_to_hash = self.block_number_to_hashes.lock().await;
// Fork? - If inserting the same block number with a different hash, remove the old ones.
match block_number_to_hash.insert(block_number, block_map.clone()) {
Some(old_block_map) if &old_block_map != block_map => {
to_remove.push(old_block_map);
// Now loop through the blocks that were building on top of the old fork and remove
// them.
let mut next_block_number = block_number.saturating_add(1);
while let Some(old_block_map) = block_number_to_hash.remove(&next_block_number) {
to_remove.push(old_block_map);
next_block_number = next_block_number.saturating_add(1);
}
},
_ => {},
}
if let Some(keep_latest_n_blocks) = self.keep_latest_n_blocks {
// If we have more blocks than we should keep, remove the oldest ones by count
// (not by block number range, to handle gaps correctly)
while block_number_to_hash.len() > keep_latest_n_blocks {
// Remove the block with the smallest number (first in BTreeMap)
if let Some((_, block_map)) = block_number_to_hash.pop_first() {
to_remove.push(block_map);
}
}
}
// Release the lock.
drop(block_number_to_hash);
if !to_remove.is_empty() {
log::trace!(target: LOG_TARGET, "Pruning old blocks: {to_remove:?}");
self.remove(&to_remove).await?;
}
Ok(())
}
/// Insert receipts into the provider.
///
/// Note: Can be merged into `insert_block_receipts` once <https://github.com/pezkuwichain/kurdistan-sdk/issues/188> is fixed and subxt let
/// us create Mock `BizinikiwiBlock`
async fn insert(
&self,
block: &impl BlockInfo,
receipts: &[(TransactionSigned, ReceiptInfo)],
ethereum_hash: &H256,
) -> Result<(), ClientError> {
let bizinikiwi_block_hash = block.hash();
let bizinikiwi_hash_ref = bizinikiwi_block_hash.as_ref();
let block_number = block.number() as i64;
let ethereum_hash_ref = ethereum_hash.as_ref();
let block_map = BlockHashMap::new(bizinikiwi_block_hash, *ethereum_hash);
log::trace!(target: LOG_TARGET, "Insert receipts for bizinikiwi block #{block_number} {:?}", bizinikiwi_block_hash);
self.prune_blocks(block.number(), &block_map).await?;
// Check if mapping already exists (eg. added when processing best block and we are now
// processing finalized block)
let result = sqlx::query!(
r#"SELECT EXISTS(SELECT 1 FROM eth_to_bizinikiwi_blocks WHERE bizinikiwi_block_hash = $1) AS "exists!:bool""#, bizinikiwi_hash_ref
)
.fetch_one(&self.pool)
.await?;
// Assuming that if no mapping exists then no relevant entries in transaction_hashes and
// logs exist
if !result.exists {
for (_, receipt) in receipts {
let transaction_hash: &[u8] = receipt.transaction_hash.as_ref();
let transaction_index = receipt.transaction_index.as_u32() as i32;
query!(
r#"
INSERT INTO transaction_hashes (transaction_hash, block_hash, transaction_index)
VALUES ($1, $2, $3)
"#,
transaction_hash,
bizinikiwi_hash_ref,
transaction_index
)
.execute(&self.pool)
.await?;
for log in &receipt.logs {
let log_index = log.log_index.as_u32() as i32;
let address: &[u8] = log.address.as_ref();
let topic_0 = log.topics.first().as_ref().map(|v| &v[..]);
let topic_1 = log.topics.get(1).as_ref().map(|v| &v[..]);
let topic_2 = log.topics.get(2).as_ref().map(|v| &v[..]);
let topic_3 = log.topics.get(3).as_ref().map(|v| &v[..]);
let data = log.data.as_ref().map(|v| &v.0[..]);
query!(
r#"
INSERT INTO logs(
block_hash,
transaction_index,
log_index,
address,
block_number,
transaction_hash,
topic_0, topic_1, topic_2, topic_3,
data)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
"#,
ethereum_hash_ref,
transaction_index,
log_index,
address,
block_number,
transaction_hash,
topic_0,
topic_1,
topic_2,
topic_3,
data
)
.execute(&self.pool)
.await?;
}
}
// Insert block mapping from Ethereum to Bizinikiwi hash
self.insert_block_mapping(&block_map).await?;
}
Ok(())
}
/// Get logs that match the given filter.
pub async fn logs(&self, filter: Option<Filter>) -> anyhow::Result<Vec<Log>> {
let mut qb = QueryBuilder::<Sqlite>::new("SELECT logs.* FROM logs WHERE 1=1");
let filter = filter.unwrap_or_default();
let latest_block = U256::from(self.block_provider.latest_block_number().await);
let as_block_number = |block_param| match block_param {
None => Ok(None),
Some(BlockNumberOrTag::U256(v)) => Ok(Some(v)),
Some(BlockNumberOrTag::BlockTag(BlockTag::Latest)) => Ok(Some(latest_block)),
Some(BlockNumberOrTag::BlockTag(tag)) => anyhow::bail!("Unsupported tag: {tag:?}"),
};
let from_block = as_block_number(filter.from_block)?;
let to_block = as_block_number(filter.to_block)?;
match (from_block, to_block, filter.block_hash) {
(Some(_), _, Some(_)) | (_, Some(_), Some(_)) => {
anyhow::bail!("block number and block hash cannot be used together");
},
(Some(block), _, _) | (_, Some(block), _) if block > latest_block => {
anyhow::bail!("block number exceeds latest block");
},
(Some(from_block), Some(to_block), None) if from_block > to_block => {
anyhow::bail!("invalid block range params");
},
(Some(from_block), Some(to_block), None) if from_block == to_block => {
qb.push(" AND block_number = ").push_bind(from_block.as_u64() as i64);
},
(Some(from_block), Some(to_block), None) => {
qb.push(" AND block_number BETWEEN ")
.push_bind(from_block.as_u64() as i64)
.push(" AND ")
.push_bind(to_block.as_u64() as i64);
},
(Some(from_block), None, None) => {
qb.push(" AND block_number >= ").push_bind(from_block.as_u64() as i64);
},
(None, Some(to_block), None) => {
qb.push(" AND block_number <= ").push_bind(to_block.as_u64() as i64);
},
(None, None, Some(hash)) => {
qb.push(" AND block_hash = ").push_bind(hash.0.to_vec());
},
(None, None, None) => {
qb.push(" AND block_number = ").push_bind(latest_block.as_u64() as i64);
},
}
if let Some(addresses) = filter.address {
match addresses {
AddressOrAddresses::Address(addr) => {
qb.push(" AND address = ").push_bind(addr.0.to_vec());
},
AddressOrAddresses::Addresses(addrs) => {
qb.push(" AND address IN (");
let mut separated = qb.separated(", ");
for addr in addrs {
separated.push_bind(addr.0.to_vec());
}
separated.push_unseparated(")");
},
}
}
if let Some(topics) = filter.topics {
if topics.len() > 4 {
return Err(anyhow::anyhow!("exceed max topics"));
}
for (i, topic) in topics.into_iter().enumerate() {
match topic {
FilterTopic::Single(hash) => {
qb.push(format_args!(" AND topic_{i} = ")).push_bind(hash.0.to_vec());
},
FilterTopic::Multiple(hashes) => {
qb.push(format_args!(" AND topic_{i} IN ("));
let mut separated = qb.separated(", ");
for hash in hashes {
separated.push_bind(hash.0.to_vec());
}
separated.push_unseparated(")");
},
}
}
}
qb.push(" LIMIT 10000");
let logs = qb
.build()
.try_map(|row| {
let block_hash: Vec<u8> = row.try_get("block_hash")?;
let transaction_index: i64 = row.try_get("transaction_index")?;
let log_index: i64 = row.try_get("log_index")?;
let address: Vec<u8> = row.try_get("address")?;
let block_number: i64 = row.try_get("block_number")?;
let transaction_hash: Vec<u8> = row.try_get("transaction_hash")?;
let topic_0: Option<Vec<u8>> = row.try_get("topic_0")?;
let topic_1: Option<Vec<u8>> = row.try_get("topic_1")?;
let topic_2: Option<Vec<u8>> = row.try_get("topic_2")?;
let topic_3: Option<Vec<u8>> = row.try_get("topic_3")?;
let data: Option<Vec<u8>> = row.try_get("data")?;
let topics = [topic_0, topic_1, topic_2, topic_3]
.iter()
.filter_map(|t| t.as_ref().map(|t| H256::from_slice(t)))
.collect::<Vec<_>>();
Ok(Log {
address: Address::from_slice(&address),
block_hash: H256::from_slice(&block_hash),
block_number: U256::from(block_number as u64),
data: data.map(Bytes::from),
log_index: U256::from(log_index as u64),
topics,
transaction_hash: H256::from_slice(&transaction_hash),
transaction_index: U256::from(transaction_index as u64),
removed: false,
})
})
.fetch_all(&self.pool)
.await?;
Ok(logs)
}
/// Get the number of receipts per block.
pub async fn receipts_count_per_block(&self, block_hash: &H256) -> Option<usize> {
let block_hash = block_hash.as_ref();
let row = query!(
r#"
SELECT COUNT(*) as count
FROM transaction_hashes
WHERE block_hash = $1
"#,
block_hash
)
.fetch_one(&self.pool)
.await
.ok()?;
let count = row.count as usize;
Some(count)
}
/// Return all transaction hashes for the given block hash.
pub async fn block_transaction_hashes(
&self,
block_hash: &H256,
) -> Option<HashMap<usize, H256>> {
let block_hash = block_hash.as_ref();
let rows = query!(
r#"
SELECT transaction_index, transaction_hash
FROM transaction_hashes
WHERE block_hash = $1
"#,
block_hash
)
.map(|row| {
let transaction_index = row.transaction_index as usize;
let transaction_hash = H256::from_slice(&row.transaction_hash);
(transaction_index, transaction_hash)
})
.fetch_all(&self.pool)
.await
.ok()?;
Some(rows.into_iter().collect())
}
/// Get the receipt for the given block hash and transaction index.
pub async fn receipt_by_block_hash_and_index(
&self,
block_hash: &H256,
transaction_index: usize,
) -> Option<ReceiptInfo> {
let block = self.block_provider.block_by_hash(block_hash).await.ok()??;
let (_, receipt) = self
.receipt_extractor
.extract_from_transaction(&block, transaction_index)
.await
.ok()?;
Some(receipt)
}
/// Get the receipt for the given transaction hash.
pub async fn receipt_by_hash(&self, transaction_hash: &H256) -> Option<ReceiptInfo> {
let (block_hash, transaction_index) = self.find_transaction(transaction_hash).await?;
let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
let (_, receipt) = self
.receipt_extractor
.extract_from_transaction(&block, transaction_index)
.await
.ok()?;
Some(receipt)
}
/// Get the signed transaction for the given transaction hash.
pub async fn signed_tx_by_hash(&self, transaction_hash: &H256) -> Option<TransactionSigned> {
let (block_hash, transaction_index) = self.find_transaction(transaction_hash).await?;
let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
let (signed_tx, _) = self
.receipt_extractor
.extract_from_transaction(&block, transaction_index)
.await
.ok()?;
Some(signed_tx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{MockBlockInfo, MockBlockInfoProvider};
use pezpallet_revive::evm::{ReceiptInfo, TransactionSigned};
use pezsp_core::{H160, H256};
use pretty_assertions::assert_eq;
use sqlx::SqlitePool;
async fn count(pool: &SqlitePool, table: &str, block_hash: Option<H256>) -> usize {
let count: i64 = match block_hash {
None =>
sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table}"))
.fetch_one(pool)
.await,
Some(hash) =>
sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table} WHERE block_hash = ?"))
.bind(hash.as_ref())
.fetch_one(pool)
.await,
}
.unwrap();
count as _
}
async fn setup_sqlite_provider(pool: SqlitePool) -> ReceiptProvider<MockBlockInfoProvider> {
ReceiptProvider {
pool,
block_provider: MockBlockInfoProvider {},
receipt_extractor: ReceiptExtractor::new_mock(),
keep_latest_n_blocks: Some(10),
block_number_to_hashes: Default::default(),
}
}
#[sqlx::test]
async fn test_insert_remove(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let block = MockBlockInfo { hash: H256::default(), number: 0 };
let receipts = vec![(
TransactionSigned::default(),
ReceiptInfo {
logs: vec![Log { block_hash: block.hash, ..Default::default() }],
..Default::default()
},
)];
let ethereum_hash = H256::from([1_u8; 32]);
let block_map = BlockHashMap::new(block.hash(), ethereum_hash);
provider.insert(&block, &receipts, &ethereum_hash).await?;
let row = provider.find_transaction(&receipts[0].1.transaction_hash).await;
assert_eq!(row, Some((block.hash, 0)));
provider.remove(&[block_map]).await?;
assert_eq!(count(&provider.pool, "transaction_hashes", Some(block.hash())).await, 0);
assert_eq!(count(&provider.pool, "logs", Some(block.hash())).await, 0);
Ok(())
}
#[sqlx::test]
async fn test_prune(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let n = provider.keep_latest_n_blocks.unwrap();
for i in 0..2 * n {
let block = MockBlockInfo { hash: H256::from([i as u8; 32]), number: i as _ };
let transaction_hash = H256::from([i as u8; 32]);
let receipts = vec![(
TransactionSigned::default(),
ReceiptInfo {
transaction_hash,
logs: vec![Log {
block_hash: block.hash,
transaction_hash,
..Default::default()
}],
..Default::default()
},
)];
let ethereum_hash = H256::from([(i + 1) as u8; 32]);
provider.insert(&block, &receipts, &ethereum_hash).await?;
}
assert_eq!(count(&provider.pool, "transaction_hashes", None).await, n);
assert_eq!(count(&provider.pool, "logs", None).await, n);
assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, n);
assert_eq!(provider.block_number_to_hashes.lock().await.len(), n);
return Ok(());
}
#[sqlx::test]
async fn test_fork(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let build_block = |seed, number| {
let block = MockBlockInfo { hash: H256::from([seed; 32]), number };
let transaction_hash = H256::from([seed; 32]);
let receipts = vec![(
TransactionSigned::default(),
ReceiptInfo {
transaction_hash,
logs: vec![Log {
block_hash: block.hash,
transaction_hash,
..Default::default()
}],
..Default::default()
},
)];
let ethereum_hash = H256::from([seed + 1; 32]);
(block, receipts, ethereum_hash)
};
// Build 4 blocks on consecutive heights: 0,1,2,3.
let (block0, receipts, ethereum_hash_0) = build_block(0, 0);
provider.insert(&block0, &receipts, &ethereum_hash_0).await?;
let (block1, receipts, ethereum_hash_1) = build_block(1, 1);
provider.insert(&block1, &receipts, &ethereum_hash_1).await?;
let (block2, receipts, ethereum_hash_2) = build_block(2, 2);
provider.insert(&block2, &receipts, &ethereum_hash_2).await?;
let (block3, receipts, ethereum_hash_3) = build_block(3, 3);
provider.insert(&block3, &receipts, &ethereum_hash_3).await?;
assert_eq!(count(&provider.pool, "transaction_hashes", None).await, 4);
assert_eq!(count(&provider.pool, "logs", None).await, 4);
assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 4);
assert_eq!(
provider.block_number_to_hashes.lock().await.clone(),
[
(0, BlockHashMap::new(block0.hash, ethereum_hash_0)),
(1, BlockHashMap::new(block1.hash, ethereum_hash_1)),
(2, BlockHashMap::new(block2.hash, ethereum_hash_2)),
(3, BlockHashMap::new(block3.hash, ethereum_hash_3))
]
.into(),
);
// Now build another block on height 1.
let (fork_block, receipts, ethereum_hash_fork) = build_block(4, 1);
provider.insert(&fork_block, &receipts, &ethereum_hash_fork).await?;
assert_eq!(count(&provider.pool, "transaction_hashes", None).await, 2);
assert_eq!(count(&provider.pool, "logs", None).await, 2);
assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 2);
assert_eq!(
provider.block_number_to_hashes.lock().await.clone(),
[
(0, BlockHashMap::new(block0.hash, ethereum_hash_0)),
(1, BlockHashMap::new(fork_block.hash, ethereum_hash_fork))
]
.into(),
);
return Ok(());
}
#[sqlx::test]
async fn test_receipts_count_per_block(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let block = MockBlockInfo { hash: H256::default(), number: 0 };
let receipts = vec![
(
TransactionSigned::default(),
ReceiptInfo { transaction_hash: H256::from([0u8; 32]), ..Default::default() },
),
(
TransactionSigned::default(),
ReceiptInfo { transaction_hash: H256::from([1u8; 32]), ..Default::default() },
),
];
let ethereum_hash = H256::from([2u8; 32]);
provider.insert(&block, &receipts, &ethereum_hash).await?;
let count = provider.receipts_count_per_block(&block.hash).await;
assert_eq!(count, Some(2));
Ok(())
}
#[sqlx::test]
async fn test_query_logs(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let block1 = MockBlockInfo { hash: H256::from([1u8; 32]), number: 1 };
let block2 = MockBlockInfo { hash: H256::from([2u8; 32]), number: 2 };
let ethereum_hash1 = H256::from([3u8; 32]);
let ethereum_hash2 = H256::from([4u8; 32]);
let log1 = Log {
block_hash: ethereum_hash1,
block_number: block1.number.into(),
address: H160::from([1u8; 20]),
topics: vec![H256::from([1u8; 32]), H256::from([2u8; 32])],
data: Some(vec![0u8; 32].into()),
transaction_hash: H256::default(),
transaction_index: U256::from(1),
log_index: U256::from(1),
..Default::default()
};
let log2 = Log {
block_hash: ethereum_hash2,
block_number: block2.number.into(),
address: H160::from([2u8; 20]),
topics: vec![H256::from([2u8; 32]), H256::from([3u8; 32])],
transaction_hash: H256::from([1u8; 32]),
transaction_index: U256::from(2),
log_index: U256::from(1),
..Default::default()
};
provider
.insert(
&block1,
&vec![(
TransactionSigned::default(),
ReceiptInfo {
logs: vec![log1.clone()],
transaction_hash: log1.transaction_hash,
transaction_index: log1.transaction_index,
..Default::default()
},
)],
&ethereum_hash1,
)
.await?;
provider
.insert(
&block2,
&vec![(
TransactionSigned::default(),
ReceiptInfo {
logs: vec![log2.clone()],
transaction_hash: log2.transaction_hash,
transaction_index: log2.transaction_index,
..Default::default()
},
)],
&ethereum_hash2,
)
.await?;
// Empty filter
let logs = provider.logs(None).await?;
assert_eq!(logs, vec![log2.clone()]);
// from_block filter
let logs = provider
.logs(Some(Filter { from_block: Some(log2.block_number.into()), ..Default::default() }))
.await?;
assert_eq!(logs, vec![log2.clone()]);
// from_block filter (using latest block)
let logs = provider
.logs(Some(Filter { from_block: Some(BlockTag::Latest.into()), ..Default::default() }))
.await?;
assert_eq!(logs, vec![log2.clone()]);
// to_block filter
let logs = provider
.logs(Some(Filter { to_block: Some(log1.block_number.into()), ..Default::default() }))
.await?;
assert_eq!(logs, vec![log1.clone()]);
// block_hash filter
let logs = provider
.logs(Some(Filter { block_hash: Some(log1.block_hash), ..Default::default() }))
.await?;
assert_eq!(logs, vec![log1.clone()]);
// single address
let logs = provider
.logs(Some(Filter {
from_block: Some(U256::from(0).into()),
address: Some(log1.address.into()),
..Default::default()
}))
.await?;
assert_eq!(logs, vec![log1.clone()]);
// multiple addresses
let logs = provider
.logs(Some(Filter {
from_block: Some(U256::from(0).into()),
address: Some(vec![log1.address, log2.address].into()),
..Default::default()
}))
.await?;
assert_eq!(logs, vec![log1.clone(), log2.clone()]);
// single topic
let logs = provider
.logs(Some(Filter {
from_block: Some(U256::from(0).into()),
topics: Some(vec![FilterTopic::Single(log1.topics[0])]),
..Default::default()
}))
.await?;
assert_eq!(logs, vec![log1.clone()]);
// multiple topic
let logs = provider
.logs(Some(Filter {
from_block: Some(U256::from(0).into()),
topics: Some(vec![
FilterTopic::Single(log1.topics[0]),
FilterTopic::Single(log1.topics[1]),
]),
..Default::default()
}))
.await?;
assert_eq!(logs, vec![log1.clone()]);
// multiple topic for topic_0
let logs = provider
.logs(Some(Filter {
from_block: Some(U256::from(0).into()),
topics: Some(vec![FilterTopic::Multiple(vec![log1.topics[0], log2.topics[0]])]),
..Default::default()
}))
.await?;
assert_eq!(logs, vec![log1.clone(), log2.clone()]);
// Altogether
let logs = provider
.logs(Some(Filter {
from_block: Some(log1.block_number.into()),
to_block: Some(log2.block_number.into()),
block_hash: None,
address: Some(vec![log1.address, log2.address].into()),
topics: Some(vec![FilterTopic::Multiple(vec![log1.topics[0], log2.topics[0]])]),
}))
.await?;
assert_eq!(logs, vec![log1.clone(), log2.clone()]);
Ok(())
}
#[sqlx::test]
async fn test_block_mapping_insert_get(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let ethereum_hash = H256::from([1u8; 32]);
let bizinikiwi_hash = H256::from([2u8; 32]);
let block_map = BlockHashMap::new(bizinikiwi_hash, ethereum_hash);
// Insert mapping
provider.insert_block_mapping(&block_map).await?;
// Test forward lookup
let resolved = provider.get_bizinikiwi_hash(&ethereum_hash).await;
assert_eq!(resolved, Some(bizinikiwi_hash));
// Test reverse lookup
let resolved = provider.get_ethereum_hash(&bizinikiwi_hash).await;
assert_eq!(resolved, Some(ethereum_hash));
Ok(())
}
#[sqlx::test]
async fn test_block_mapping_remove(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let ethereum_hash1 = H256::from([1u8; 32]);
let ethereum_hash2 = H256::from([2u8; 32]);
let bizinikiwi_hash1 = H256::from([3u8; 32]);
let bizinikiwi_hash2 = H256::from([4u8; 32]);
let block_map1 = BlockHashMap::new(bizinikiwi_hash1, ethereum_hash1);
let block_map2 = BlockHashMap::new(bizinikiwi_hash2, ethereum_hash2);
// Insert mappings
provider.insert_block_mapping(&block_map1).await?;
provider.insert_block_mapping(&block_map2).await?;
// Verify they exist
assert_eq!(
provider.get_bizinikiwi_hash(&block_map1.ethereum_hash).await,
Some(block_map1.bizinikiwi_hash)
);
assert_eq!(
provider.get_bizinikiwi_hash(&block_map2.ethereum_hash).await,
Some(block_map2.bizinikiwi_hash)
);
// Remove one mapping
provider.remove(&[block_map1]).await?;
// Verify removal
assert_eq!(provider.get_bizinikiwi_hash(&ethereum_hash1).await, None);
assert_eq!(provider.get_bizinikiwi_hash(&ethereum_hash2).await, Some(bizinikiwi_hash2));
Ok(())
}
#[sqlx::test]
async fn test_block_mapping_pruning_integration(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let ethereum_hash = H256::from([1u8; 32]);
let bizinikiwi_hash = H256::from([2u8; 32]);
let block_map = BlockHashMap::new(bizinikiwi_hash, ethereum_hash);
// Insert mapping
provider.insert_block_mapping(&block_map).await?;
assert_eq!(
provider.get_bizinikiwi_hash(&block_map.ethereum_hash).await,
Some(block_map.bizinikiwi_hash)
);
// Remove bizinikiwi block (this should also remove the mapping)
provider.remove(&[block_map.clone()]).await?;
// Mapping should be gone
assert_eq!(provider.get_bizinikiwi_hash(&block_map.ethereum_hash).await, None);
Ok(())
}
#[sqlx::test]
async fn test_logs_with_ethereum_block_hash_mapping(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
let ethereum_hash = H256::from([1u8; 32]);
let bizinikiwi_hash = H256::from([2u8; 32]);
let block_number = 1u64;
// Create a log with ethereum hash
let log = Log {
block_hash: ethereum_hash,
block_number: block_number.into(),
address: H160::from([1u8; 20]),
topics: vec![H256::from([1u8; 32])],
transaction_hash: H256::from([3u8; 32]),
transaction_index: U256::from(0),
log_index: U256::from(0),
data: Some(vec![0u8; 32].into()),
..Default::default()
};
// Insert the log
let block = MockBlockInfo { hash: bizinikiwi_hash, number: block_number as u32 };
let receipts = vec![(
TransactionSigned::default(),
ReceiptInfo {
logs: vec![log.clone()],
transaction_hash: log.transaction_hash,
transaction_index: log.transaction_index,
..Default::default()
},
)];
provider.insert(&block, &receipts, &ethereum_hash).await?;
// Query logs using Ethereum block hash (should resolve to bizinikiwi hash)
let logs = provider
.logs(Some(Filter { block_hash: Some(ethereum_hash), ..Default::default() }))
.await?;
assert_eq!(logs, vec![log]);
Ok(())
}
#[sqlx::test]
async fn test_mapping_count(pool: SqlitePool) -> anyhow::Result<()> {
let provider = setup_sqlite_provider(pool).await;
// Initially no mappings
assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 0);
let block_map1 = BlockHashMap::new(H256::from([1u8; 32]), H256::from([2u8; 32]));
let block_map2 = BlockHashMap::new(H256::from([3u8; 32]), H256::from([4u8; 32]));
// Insert some mappings
provider.insert_block_mapping(&block_map1).await?;
provider.insert_block_mapping(&block_map2).await?;
assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 2);
// Remove one
provider.remove(&[block_map1]).await?;
assert_eq!(count(&provider.pool, "eth_to_bizinikiwi_blocks", None).await, 1);
Ok(())
}
}