use std::fmt::Debug; use std::hash::Hash; use jsonrpsee::core::client::ClientT; use crate::{EthereumFinality, ForeignChainInspectionError, ForeignChainInspector}; use foreign_chain_rpc_interfaces::evm::{ BlockNumberOrTag, FinalityTag, GetBlockByNumberArgs, GetBlockByNumberResponse, GetTransactionReceiptARgs, GetTransactionReceiptResponse, H256, Log, ReturnFullTransactionObjects, U64, }; const GET_TRANSACTION_RECEIPT_METHOD: &str = "eth_getTransactionReceipt"; const GET_BLOCK_BY_NUMBER_METHOD: &str = "eth_getBlockByNumber"; /// Marker trait for EVM-compatible chain type parameters. /// /// Each chain provides its own block-hash and transaction-hash newtypes so that /// different chains remain type-incompatible at the call site, while sharing the /// single [`EvmInspector`] implementation. pub trait EvmChain { type BlockHash: From<[u8; 32]> + Into<[u8; 32]> + Clone + Debug + PartialEq + Eq + Hash + Send; type TransactionHash: From<[u8; 32]> + Into<[u8; 32]> + Clone + Debug + PartialEq + Eq + Hash + Send; } #[derive(Clone)] pub struct EvmInspector { client: Client, _chain: std::marker::PhantomData, } impl ForeignChainInspector for EvmInspector where Client: ClientT + Send + Sync, Chain: EvmChain + Send + Sync, { type TransactionId = Chain::TransactionHash; type Finality = EthereumFinality; type Extractor = EvmExtractor; type ExtractedValue = EvmExtractedValue; async fn extract( &self, transaction: Chain::TransactionHash, finality: EthereumFinality, extractors: Vec, ) -> Result>, ForeignChainInspectionError> { let get_transaction_receipt_args = GetTransactionReceiptARgs { transaction_hash: H256(transaction.into()), }; let transaction_receipt: GetTransactionReceiptResponse = self .client .request( GET_TRANSACTION_RECEIPT_METHOD, &get_transaction_receipt_args, ) .await?; self.verify_finality_level(transaction_receipt.block_number, finality) .await?; self.verify_block_is_canonical( transaction_receipt.block_number, transaction_receipt.block_hash, ) .await?; let transaction_success = transaction_receipt.status == U64::one(); if !transaction_success { return Err(ForeignChainInspectionError::TransactionFailed); } extractors .iter() .map(|extractor| extractor.extract_value(&transaction_receipt)) .collect() } } impl EvmInspector where Client: ClientT + Send + Sync, Chain: EvmChain, { pub fn new(client: Client) -> Self { Self { client, _chain: std::marker::PhantomData, } } /// Checks that the receipt's block has reached the requested finality level — i.e. that the /// head of the chain at `finality` is at or past `receipt_block_number`. async fn verify_finality_level( &self, receipt_block_number: U64, finality: EthereumFinality, ) -> Result<(), ForeignChainInspectionError> { let finality_tag = match finality { EthereumFinality::Finalized => FinalityTag::Finalized, EthereumFinality::Safe => FinalityTag::Safe, EthereumFinality::Latest => FinalityTag::Latest, }; let args = GetBlockByNumberArgs::new( BlockNumberOrTag::Tag(finality_tag), ReturnFullTransactionObjects::from(false), ); let head: GetBlockByNumberResponse = self .client .request(GET_BLOCK_BY_NUMBER_METHOD, &args) .await?; if head.number < receipt_block_number { return Err(ForeignChainInspectionError::NotFinalized); } Ok(()) } /// Checks that the receipt's block is on the canonical chain by re-fetching the canonical /// block at `receipt_block_number` and comparing hashes. `eth_getBlockByNumber` only ever /// resolves to a canonical block, so a mismatch means the receipt was indexed against a /// side block (stale tx index, partially-applied reorg, divergent RPC backend, etc.). /// /// The canonical block's height is also asserted against the requested one — a divergent /// RPC that returns a hash from a different height would otherwise sneak past a /// hash-only check. async fn verify_block_is_canonical( &self, receipt_block_number: U64, receipt_block_hash: H256, ) -> Result<(), ForeignChainInspectionError> { let args = GetBlockByNumberArgs::new( BlockNumberOrTag::Number(receipt_block_number), ReturnFullTransactionObjects::from(false), ); let canonical: GetBlockByNumberResponse = self .client .request(GET_BLOCK_BY_NUMBER_METHOD, &args) .await?; let hash_matches = canonical.hash == receipt_block_hash; let height_matches = canonical.number == receipt_block_number; if !hash_matches || !height_matches { return Err(ForeignChainInspectionError::NonCanonicalBlock { block_number: receipt_block_number.as_u64(), receipt_hash: receipt_block_hash.into(), canonical_hash: canonical.hash.into(), }); } Ok(()) } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum EvmExtractedValue { BlockHash(Chain::BlockHash), Log(Log), } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum EvmExtractor { BlockHash, Log { log_index: u64 }, } impl EvmExtractor { fn extract_value( &self, rpc_response: &GetTransactionReceiptResponse, ) -> Result, ForeignChainInspectionError> { match self { EvmExtractor::BlockHash => Ok(EvmExtractedValue::BlockHash(From::from( *rpc_response.block_hash.as_fixed_bytes(), ))), EvmExtractor::Log { log_index } => { let target_index = ethereum_types::U64::from(*log_index); let log = rpc_response .logs .iter() .find(|log| log.log_index == target_index) .cloned() .ok_or(ForeignChainInspectionError::LogIndexOutOfBounds)?; Ok(EvmExtractedValue::Log(log)) } } } }