From 305c68ffed9bed2eb89f2aa25d71f8247acfd8cf Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:10:36 +0200 Subject: [PATCH] add state and block overrides in `eth_call` (#1299) * add state and block overrides in eth_call * change error naming * fix tests * fmt * Update Makefile * some fixes with databaseRef * fix tests * fix tests * rm useless test * fix comments * Update Makefile * fix comments * simplify error handling in test * fmt * rm useless block_in_place --- src/eth_provider/contracts/erc20.rs | 2 + src/eth_provider/database/mod.rs | 1 + src/eth_provider/database/state.rs | 107 ++++++++++++++++++ src/eth_provider/error.rs | 32 +++++- src/eth_provider/provider.rs | 88 ++++++++++++++- src/eth_rpc/api/eth_api.rs | 14 ++- src/eth_rpc/servers/eth_rpc.rs | 15 ++- src/tracing/builder.rs | 11 +- src/tracing/database.rs | 123 --------------------- src/tracing/mod.rs | 88 +++++++-------- tests/tests/eth_provider.rs | 165 +++++++++++++++++++++++++++- 11 files changed, 451 insertions(+), 195 deletions(-) create mode 100644 src/eth_provider/database/state.rs delete mode 100644 src/tracing/database.rs diff --git a/src/eth_provider/contracts/erc20.rs b/src/eth_provider/contracts/erc20.rs index c1355fec9..07cf4e26f 100644 --- a/src/eth_provider/contracts/erc20.rs +++ b/src/eth_provider/contracts/erc20.rs @@ -111,6 +111,8 @@ impl EthereumErc20

{ ..Default::default() }, Some(block_id), + None, + None, ) .await } diff --git a/src/eth_provider/database/mod.rs b/src/eth_provider/database/mod.rs index 39d2cf27b..76ebe5850 100644 --- a/src/eth_provider/database/mod.rs +++ b/src/eth_provider/database/mod.rs @@ -1,5 +1,6 @@ pub mod ethereum; pub mod filter; +pub mod state; pub mod types; use super::error::KakarotError; diff --git a/src/eth_provider/database/state.rs b/src/eth_provider/database/state.rs new file mode 100644 index 000000000..650148007 --- /dev/null +++ b/src/eth_provider/database/state.rs @@ -0,0 +1,107 @@ +use crate::eth_provider::{error::EthApiError, provider::EthereumProvider}; +use reth_primitives::{Address, B256, U256}; +use reth_revm::{ + db::CacheDB, + primitives::{AccountInfo, Bytecode}, + DatabaseRef, +}; +use reth_rpc_types::{serde_helpers::JsonStorageKey, BlockHashOrNumber, BlockId, BlockNumberOrTag}; +use tokio::runtime::Handle; + +#[derive(Debug, Clone)] +pub struct EthCacheDatabase(pub CacheDB>); + +/// Ethereum database type. +#[derive(Debug, Clone)] +#[allow(clippy::redundant_pub_crate)] +pub struct EthDatabase { + /// The Ethereum provider. + provider: P, + /// The block ID. + block_id: BlockId, +} + +impl EthDatabase

{ + pub(crate) const fn new(provider: P, block_id: BlockId) -> Self { + Self { provider, block_id } + } +} + +/// The [`DatabaseRef`] trait implementation for [`EthDatabase`]. +/// +/// This implementation is designed to handle database interactions in a manner that is compatible +/// with both synchronous and asynchronous Rust contexts. Given the constraints of the underlying +/// database operations, it's necessary to perform blocking calls in a controlled manner to avoid +/// blocking the asynchronous runtime. +/// +/// ### Why Use `tokio::task::block_in_place`? +/// +/// The `tokio::task::block_in_place` function is employed here to enter a blocking context safely +/// within an asynchronous environment. This allows the blocking database operations to be executed +/// without hindering the performance of other asynchronous tasks or blocking the runtime. +impl DatabaseRef for EthDatabase

{ + type Error = EthApiError; + + /// Returns the account information for the given address without caching. + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + tokio::task::block_in_place(|| { + let account_info = Handle::current().block_on(async { + let bytecode = self.provider.get_code(address, Some(self.block_id)).await?; + let bytecode = Bytecode::new_raw(bytecode); + let code_hash = bytecode.hash_slow(); + + let nonce = self.provider.transaction_count(address, Some(self.block_id)).await?.to(); + let balance = self.provider.balance(address, Some(self.block_id)).await?; + + Result::<_, EthApiError>::Ok(AccountInfo { nonce, balance, code: Some(bytecode), code_hash }) + })?; + + Ok(Some(account_info)) + }) + } + + /// Returns the code for the given code hash. + /// TODO: Implement this method in the provider + fn code_by_hash_ref(&self, _code_hash: B256) -> Result { + Ok(Default::default()) + } + + /// Returns the storage value for the given address and index without caching. + fn storage_ref(&self, address: Address, index: U256) -> Result { + tokio::task::block_in_place(|| { + let storage = Handle::current().block_on(async { + let value = self + .provider + .storage_at( + address, + JsonStorageKey(B256::from_slice(&index.to_be_bytes::<32>())), + Some(self.block_id), + ) + .await?; + Result::<_, EthApiError>::Ok(value) + })?; + let storage = U256::from_be_bytes(storage.0); + + Ok(storage) + }) + } + + /// Returns the block hash for the given block number without caching. + fn block_hash_ref(&self, block_number: u64) -> Result { + tokio::task::block_in_place(|| { + let hash = Handle::current().block_on(async { + let hash = self + .provider + .block_by_number(BlockNumberOrTag::Number(block_number), false) + .await? + .ok_or(EthApiError::UnknownBlock(BlockHashOrNumber::Number(block_number)))? + .header + .hash + .unwrap_or_default(); + Result::<_, EthApiError>::Ok(hash) + })?; + + Ok(hash) + }) + } +} diff --git a/src/eth_provider/error.rs b/src/eth_provider/error.rs index a06a830f4..c9c23bbbb 100644 --- a/src/eth_provider/error.rs +++ b/src/eth_provider/error.rs @@ -2,7 +2,8 @@ use alloy_sol_types::decode_revert_reason; use jsonrpsee::types::ErrorObject; use num_traits::cast::ToPrimitive; use reth_primitives::{Bytes, B256}; -use reth_rpc_types::BlockHashOrNumber; +use reth_rpc_eth_types::EthApiError as RethEthApiError; +use reth_rpc_types::{BlockHashOrNumber, ToRpcError}; use starknet::core::types::Felt; use thiserror::Error; @@ -44,6 +45,12 @@ impl From<&EthApiError> for EthRpcErrorCode { } } +impl From for RethEthApiError { + fn from(value: EthApiError) -> Self { + Self::other(value) + } +} + /// Error that can occur when interacting with the ETH Api. #[derive(Debug, Error)] pub enum EthApiError { @@ -68,7 +75,7 @@ pub enum EthApiError { /// Error related to transaction calldata being too large. CalldataExceededLimit(usize, usize), /// Reth Eth API error - RethEthApi(#[from] reth_rpc_eth_types::EthApiError), + RethEthApi(#[from] RethEthApiError), } impl std::fmt::Display for EthApiError { @@ -98,16 +105,29 @@ impl std::fmt::Display for EthApiError { /// Constructs a JSON-RPC error object, consisting of `code` and `message`. impl From for ErrorObject<'static> { fn from(value: EthApiError) -> Self { + (&value).into() + } +} + +/// Constructs a JSON-RPC error object, consisting of `code` and `message`. +impl From<&EthApiError> for ErrorObject<'static> { + fn from(value: &EthApiError) -> Self { let msg = format!("{value}"); - let code = EthRpcErrorCode::from(&value); + let code = EthRpcErrorCode::from(value); let data = match value { - EthApiError::Execution(ExecutionError::Evm(EvmError::Other(ref b))) => Some(b), + EthApiError::Execution(ExecutionError::Evm(EvmError::Other(ref b))) => Some(b.clone()), _ => None, }; ErrorObject::owned(code as i32, msg, data) } } +impl ToRpcError for EthApiError { + fn to_rpc_error(&self) -> ErrorObject<'static> { + self.into() + } +} + /// Error related to the Kakarot eth provider /// which utilizes the starknet provider and /// a database internally. @@ -286,6 +306,9 @@ pub enum TransactionError { /// Thrown if the tracing fails #[error("tracing error: {0}")] Tracing(Box), + /// Thrown if the call with state or block overrides fails + #[error("tracing error: {0}")] + Call(Box), } impl From<&TransactionError> for EthRpcErrorCode { @@ -297,6 +320,7 @@ impl From<&TransactionError> for EthRpcErrorCode { | TransactionError::TipAboveFeeCap(_, _) => Self::TransactionRejected, TransactionError::ExpectedFullTransactions | TransactionError::Tracing(_) + | TransactionError::Call(_) | TransactionError::ExceedsBlockGasLimit(_, _) => Self::InternalError, } } diff --git a/src/eth_provider/provider.rs b/src/eth_provider/provider.rs index ed9bbce32..7d2680bc4 100644 --- a/src/eth_provider/provider.rs +++ b/src/eth_provider/provider.rs @@ -3,6 +3,7 @@ use super::{ database::{ ethereum::EthereumBlockStore, filter::EthDatabaseFilterBuilder, + state::{EthCacheDatabase, EthDatabase}, types::{ header::StoredHeader, log::StoredLog, @@ -26,7 +27,11 @@ use super::{ utils::{class_hash_not_declared, contract_not_found, entrypoint_not_found, split_u256}, }; use crate::{ - eth_provider::database::{ethereum::EthereumTransactionStore, filter, filter::format_hex, FindOpts}, + eth_provider::database::{ + ethereum::EthereumTransactionStore, + filter::{self, format_hex}, + FindOpts, + }, into_via_try_wrapper, into_via_wrapper, models::{ block::{EthBlockId, EthBlockNumberOrTag}, @@ -42,12 +47,22 @@ use eyre::{eyre, Result}; use itertools::Itertools; use mongodb::bson::doc; use num_traits::cast::ToPrimitive; +use reth_evm_ethereum::EthEvmConfig; +use reth_node_api::ConfigureEvm; use reth_primitives::{ Address, BlockId, BlockNumberOrTag, Bytes, TransactionSigned, TransactionSignedEcRecovered, TxKind, B256, U256, U64, }; +use reth_revm::{ + db::CacheDB, + primitives::{BlockEnv, CfgEnv, CfgEnvWithHandlerCfg, HandlerCfg, SpecId}, +}; +use reth_rpc_eth_types::{error::ensure_success, revm_utils::prepare_call_env}; use reth_rpc_types::{ - serde_helpers::JsonStorageKey, txpool::TxpoolContent, BlockHashOrNumber, FeeHistory, Filter, FilterChanges, Header, - Index, RichBlock, SyncInfo, SyncStatus, Transaction, TransactionReceipt, TransactionRequest, + serde_helpers::JsonStorageKey, + state::{EvmOverrides, StateOverride}, + txpool::TxpoolContent, + BlockHashOrNumber, BlockOverrides, FeeHistory, Filter, FilterChanges, Header, Index, RichBlock, SyncInfo, + SyncStatus, Transaction, TransactionReceipt, TransactionRequest, }; use reth_rpc_types_compat::transaction::from_recovered; #[cfg(feature = "hive")] @@ -119,7 +134,13 @@ pub trait EthereumProvider { /// Returns the logs for the given filter. async fn get_logs(&self, filter: Filter) -> EthProviderResult; /// Returns the result of a call. - async fn call(&self, request: TransactionRequest, block_id: Option) -> EthProviderResult; + async fn call( + &self, + request: TransactionRequest, + block_id: Option, + state_overrides: Option, + block_overrides: Option>, + ) -> EthProviderResult; /// Returns the result of a estimate gas. async fn estimate_gas(&self, call: TransactionRequest, block_id: Option) -> EthProviderResult; /// Returns the fee history given a block count and a newest block number. @@ -450,7 +471,64 @@ where )) } - async fn call(&self, request: TransactionRequest, block_id: Option) -> EthProviderResult { + async fn call( + &self, + request: TransactionRequest, + block_id: Option, + state_overrides: Option, + block_overrides: Option>, + ) -> EthProviderResult { + // Create the EVM overrides from the state and block overrides. + let evm_overrides = EvmOverrides::new(state_overrides, block_overrides); + + // Check if either state_overrides or block_overrides is present. + if evm_overrides.has_state() || evm_overrides.has_block() { + // Create the configuration environment with the chain ID. + let cfg_env = CfgEnv::default().with_chain_id(self.chain_id().await?.unwrap_or_default().to()); + + // Retrieve the block header details. + let Header { number, timestamp, miner, base_fee_per_gas, difficulty, .. } = + self.header(&block_id.unwrap_or_default()).await?.unwrap_or_default(); + + // Create the block environment with the retrieved header details and transaction request. + let block_env = BlockEnv { + number: U256::from(number.unwrap_or_default()), + timestamp: U256::from(timestamp), + gas_limit: U256::from(request.gas.unwrap_or_default()), + coinbase: miner, + basefee: U256::from(base_fee_per_gas.unwrap_or_default()), + prevrandao: Some(B256::from_slice(&difficulty.to_be_bytes::<32>()[..])), + ..Default::default() + }; + + // Combine the configuration environment with the handler configuration. + let cfg_env_with_handler_cfg = + CfgEnvWithHandlerCfg { cfg_env, handler_cfg: HandlerCfg::new(SpecId::CANCUN) }; + + // Create a snapshot of the Ethereum database using the block ID. + let mut db = EthCacheDatabase(CacheDB::new(EthDatabase::new(self, block_id.unwrap_or_default()))); + + // Prepare the call environment with the transaction request, gas limit, and overrides. + let env = prepare_call_env( + cfg_env_with_handler_cfg, + block_env, + request.clone(), + request.gas.unwrap_or_default().try_into().expect("Gas limit is too large"), + &mut db.0, + evm_overrides, + )?; + + // Execute the transaction using the configured EVM asynchronously. + let res = EthEvmConfig::default() + .evm_with_env(db.0, env) + .transact() + .map_err(|err| >::into(TransactionError::Call(err.into())))?; + + // Ensure the transaction was successful and return the result. + return Ok(ensure_success(res.result)?); + } + + // If no state or block overrides are present, call the helper function to execute the call. let output = self.call_helper(request, block_id).await?; Ok(Bytes::from(output.0.into_iter().filter_map(|x| x.to_u8()).collect::>())) } diff --git a/src/eth_rpc/api/eth_api.rs b/src/eth_rpc/api/eth_api.rs index 9459922f1..74b34ec50 100644 --- a/src/eth_rpc/api/eth_api.rs +++ b/src/eth_rpc/api/eth_api.rs @@ -1,9 +1,9 @@ use jsonrpsee::{core::RpcResult as Result, proc_macros::rpc}; use reth_primitives::{Address, BlockId, BlockNumberOrTag, Bytes, B256, B64, U256, U64}; use reth_rpc_types::{ - serde_helpers::JsonStorageKey, AccessListWithGasUsed, EIP1186AccountProofResponse, FeeHistory, Filter, - FilterChanges, Index, RichBlock, SyncStatus, Transaction as EthTransaction, TransactionReceipt, TransactionRequest, - Work, + serde_helpers::JsonStorageKey, state::StateOverride, AccessListWithGasUsed, BlockOverrides, + EIP1186AccountProofResponse, FeeHistory, Filter, FilterChanges, Index, RichBlock, SyncStatus, + Transaction as EthTransaction, TransactionReceipt, TransactionRequest, Work, }; /// Ethereum JSON-RPC API Trait @@ -109,7 +109,13 @@ pub trait EthApi { /// Executes a new message call immediately without creating a transaction on the block chain. #[method(name = "call")] - async fn call(&self, request: TransactionRequest, block_id: Option) -> Result; + async fn call( + &self, + request: TransactionRequest, + block_id: Option, + state_overrides: Option, + block_overrides: Option>, + ) -> Result; /// Generates an access list for a transaction. /// diff --git a/src/eth_rpc/servers/eth_rpc.rs b/src/eth_rpc/servers/eth_rpc.rs index cd909a51a..5fb80d732 100644 --- a/src/eth_rpc/servers/eth_rpc.rs +++ b/src/eth_rpc/servers/eth_rpc.rs @@ -7,8 +7,9 @@ use crate::{ use jsonrpsee::core::{async_trait, RpcResult as Result}; use reth_primitives::{Address, BlockId, BlockNumberOrTag, Bytes, B256, B64, U256, U64}; use reth_rpc_types::{ - serde_helpers::JsonStorageKey, AccessListWithGasUsed, EIP1186AccountProofResponse, FeeHistory, Filter, - FilterChanges, Index, RichBlock, SyncStatus, Transaction, TransactionReceipt, TransactionRequest, Work, + serde_helpers::JsonStorageKey, state::StateOverride, AccessListWithGasUsed, BlockOverrides, + EIP1186AccountProofResponse, FeeHistory, Filter, FilterChanges, Index, RichBlock, SyncStatus, Transaction, + TransactionReceipt, TransactionRequest, Work, }; use serde_json::Value; @@ -154,8 +155,14 @@ where } #[tracing::instrument(skip(self, request), err)] - async fn call(&self, request: TransactionRequest, block_id: Option) -> Result { - Ok(self.eth_provider.call(request, block_id).await?) + async fn call( + &self, + request: TransactionRequest, + block_id: Option, + state_overrides: Option, + block_overrides: Option>, + ) -> Result { + Ok(self.eth_provider.call(request, block_id, state_overrides, block_overrides).await?) } async fn create_access_list( diff --git a/src/tracing/builder.rs b/src/tracing/builder.rs index b8bf1d9eb..b072c82f9 100644 --- a/src/tracing/builder.rs +++ b/src/tracing/builder.rs @@ -1,10 +1,14 @@ -use super::{database::EthDatabaseSnapshot, Tracer, TracerResult}; +use super::{Tracer, TracerResult}; use crate::eth_provider::{ + database::state::{EthCacheDatabase, EthDatabase}, error::{EthApiError, TransactionError}, provider::EthereumProvider, }; use reth_primitives::{B256, U256}; -use reth_revm::primitives::{BlockEnv, CfgEnv, Env, EnvWithHandlerCfg, HandlerCfg, SpecId}; +use reth_revm::{ + db::CacheDB, + primitives::{BlockEnv, CfgEnv, Env, EnvWithHandlerCfg, HandlerCfg, SpecId}, +}; use reth_rpc_types::{ trace::geth::{GethDebugTracingCallOptions, GethDebugTracingOptions}, Block, BlockHashOrNumber, BlockId, BlockTransactions, Header, @@ -184,7 +188,8 @@ impl TracerBuilder { let env = self.init_env_with_handler_config(); // DB should use the state of the parent block - let db = EthDatabaseSnapshot::new(self.eth_provider, self.block.header.parent_hash.into()); + let db = + EthCacheDatabase(CacheDB::new(EthDatabase::new(self.eth_provider, self.block.header.parent_hash.into()))); let tracing_options = self.tracing_options; diff --git a/src/tracing/database.rs b/src/tracing/database.rs deleted file mode 100644 index 9e98d620c..000000000 --- a/src/tracing/database.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::eth_provider::{error::EthApiError, provider::EthereumProvider}; -use reth_primitives::{Address, B256, U256}; -use reth_revm::{ - db::{AccountState, CacheDB, DbAccount}, - primitives::{Account, AccountInfo, Bytecode}, - Database, DatabaseCommit, -}; -use reth_rpc_types::{serde_helpers::JsonStorageKey, BlockHashOrNumber, BlockId, BlockNumberOrTag}; -use std::collections::HashMap; -use tokio::runtime::Handle; - -#[derive(Debug)] -#[allow(clippy::redundant_pub_crate)] -pub(crate) struct EthDatabaseSnapshot { - cache: CacheDB

, - block_id: BlockId, -} - -impl EthDatabaseSnapshot

{ - pub(crate) fn new(provider: P, block_id: BlockId) -> Self { - Self { cache: CacheDB::new(provider), block_id } - } -} - -impl Database for EthDatabaseSnapshot

{ - type Error = EthApiError; - - /// Returns the account information for the given address. - /// - /// # Panics - /// - /// Panics if called from a non-async runtime. - fn basic(&mut self, address: Address) -> Result, Self::Error> { - let cache = &self.cache; - if let Some(account) = cache.accounts.get(&address) { - return Ok(Some(account.info.clone())); - } - - let account_info = Handle::current().block_on(async { - let bytecode = cache.db.get_code(address, Some(self.block_id)).await?; - let bytecode = Bytecode::new_raw(bytecode); - let code_hash = bytecode.hash_slow(); - - let nonce = cache.db.transaction_count(address, Some(self.block_id)).await?.to(); - let balance = cache.db.balance(address, Some(self.block_id)).await?; - - Result::<_, EthApiError>::Ok(AccountInfo { nonce, balance, code: Some(bytecode), code_hash }) - })?; - - self.cache.insert_account_info(address, account_info.clone()); - Ok(Some(account_info)) - } - - /// Returns the code for the given code hash. - fn code_by_hash(&mut self, code_hash: B256) -> Result { - Ok(self.cache.contracts.get(&code_hash).cloned().unwrap_or_default()) - } - - /// Returns the storage value for the given address and index. - /// - /// # Panics - /// - /// Panics if called from a non-async runtime. - fn storage(&mut self, address: Address, index: U256) -> Result { - let cache = &self.cache; - if let Some(account) = cache.accounts.get(&address) { - return Ok(account.storage.get(&index).copied().unwrap_or_default()); - } - - let storage = Handle::current().block_on(async { - let value = cache - .db - .storage_at(address, JsonStorageKey(B256::from_slice(&index.to_be_bytes::<32>())), Some(self.block_id)) - .await?; - Result::<_, EthApiError>::Ok(value) - })?; - let storage = U256::from_be_bytes(storage.0); - - self.cache.accounts.entry(address).or_default().storage.insert(index, storage); - Ok(storage) - } - - /// Returns the block hash for the given block number. - /// - /// # Panics - /// - /// Panics if called from a non-async runtime. - fn block_hash(&mut self, block_number: u64) -> Result { - let cache = &self.cache; - let number = U256::from(block_number); - if let Some(hash) = cache.block_hashes.get(&U256::from(number)) { - return Ok(*hash); - } - - let hash = Handle::current().block_on(async { - let hash = cache - .db - .block_by_number(BlockNumberOrTag::Number(block_number), false) - .await? - .ok_or(EthApiError::UnknownBlock(BlockHashOrNumber::Number(block_number)))? - .header - .hash - .unwrap_or_default(); - Result::<_, EthApiError>::Ok(hash) - })?; - - self.cache.block_hashes.insert(number, hash); - Ok(hash) - } -} - -impl DatabaseCommit for EthDatabaseSnapshot

{ - fn commit(&mut self, changes: HashMap) { - for (address, account) in changes { - let db_account = DbAccount { - info: account.info.clone(), - storage: account.storage.into_iter().map(|(k, v)| (k, v.present_value)).collect(), - account_state: AccountState::None, - }; - self.cache.accounts.insert(address, db_account); - } - } -} diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index 83bcf5915..e9960c0e3 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -1,9 +1,8 @@ pub mod builder; -mod database; -use self::database::EthDatabaseSnapshot; use crate::{ eth_provider::{ + database::state::EthCacheDatabase, error::{EthApiError, EthereumDataFormatError, TransactionError}, provider::EthereumProvider, }, @@ -14,8 +13,8 @@ use reth_evm_ethereum::EthEvmConfig; use reth_node_api::{ConfigureEvm, ConfigureEvmEnv}; use reth_primitives::{ruint::FromUintError, B256}; use reth_revm::{ - primitives::{CfgEnvWithHandlerCfg, Env, EnvWithHandlerCfg, ExecutionResult, ResultAndState}, - Database, DatabaseCommit, + primitives::{CfgEnvWithHandlerCfg, Env, EnvWithHandlerCfg}, + DatabaseCommit, }; use reth_rpc_eth_types::revm_utils::build_call_evm_env; use reth_rpc_types::{ @@ -86,7 +85,7 @@ impl TracingResult { pub struct Tracer { transactions: Vec, env: EnvWithHandlerCfg, - db: EthDatabaseSnapshot

, + db: EthCacheDatabase

, tracing_options: TracingOptions, } @@ -94,7 +93,7 @@ impl Tracer

{ /// Traces the transaction with Geth tracing options and returns the resulting traces and state. fn trace_geth( env: EnvWithHandlerCfg, - db: &mut EthDatabaseSnapshot

, + db: &EthCacheDatabase

, tx: &reth_rpc_types::Transaction, opts: GethDebugTracingOptions, ) -> TracingStateResult { @@ -118,10 +117,13 @@ impl Tracer

{ // Build EVM with environment and inspector let eth_evm_config = EthEvmConfig::default(); - let evm = eth_evm_config.evm_with_env_and_inspector(db, env, &mut inspector); - // Execute transaction - let res = transact_in_place(evm)?; + let res = { + let mut evm = eth_evm_config.evm_with_env_and_inspector(db.0.clone(), env, &mut inspector); + + // Execute transaction + evm.transact().map_err(|err| TransactionError::Tracing(err.into()))? + }; // Get call traces let call_frame = inspector.into_geth_builder().geth_call_traces( @@ -150,8 +152,13 @@ impl Tracer

{ // Use default tracer let mut inspector = TracingInspector::new(TracingInspectorConfig::from_geth_config(&config)); let eth_evm_config = EthEvmConfig::default(); - let evm = eth_evm_config.evm_with_env_and_inspector(db, env, &mut inspector); - let res = transact_in_place(evm)?; + + let res = { + let mut evm = eth_evm_config.evm_with_env_and_inspector(db.0.clone(), env, &mut inspector); + // Execute transaction + evm.transact().map_err(|err| TransactionError::Tracing(err.into()))? + }; + let gas_used = res.result.gas_used(); let return_value = res.result.into_output().unwrap_or_default(); let frame = inspector.into_geth_builder().geth_traces(gas_used, return_value, config); @@ -164,7 +171,7 @@ impl Tracer

{ /// Traces the transaction with Parity tracing options and returns the resulting traces and state. fn trace_parity( env: EnvWithHandlerCfg, - db: &mut EthDatabaseSnapshot

, + db: &EthCacheDatabase

, tx: &reth_rpc_types::Transaction, tracing_config: TracingInspectorConfig, ) -> TracingStateResult { @@ -181,10 +188,14 @@ impl Tracer

{ // Build EVM with environment and inspector let eth_evm_config = EthEvmConfig::default(); - let evm = eth_evm_config.evm_with_env_and_inspector(db, env, &mut inspector); // Execute transaction - let res = transact_in_place(evm)?; + let res = { + let mut evm = eth_evm_config.evm_with_env_and_inspector(db.0.clone(), env, &mut inspector); + + // Execute transaction + evm.transact().map_err(|err| TransactionError::Tracing(err.into()))? + }; // Create transaction info let transaction_info = TransactionInfo::from(tx).with_base_fee(block_base_fee); @@ -226,7 +237,9 @@ impl Tracer

{ let env = env_with_tx(&self.env, tx.clone())?; let eth_evm_config = EthEvmConfig::default(); - transact_commit_in_place(eth_evm_config.evm_with_env(&mut self.db, env))?; + + let mut evm = eth_evm_config.evm_with_env(&mut self.db.0, env); + evm.transact_commit().map_err(|err| TransactionError::Tracing(err.into()))?; } Err(EthApiError::TransactionNotFound(transaction_hash)) @@ -276,13 +289,18 @@ impl Tracer

{ // Build EVM with environment and inspector. let eth_evm_config = EthEvmConfig::default(); - let evm = eth_evm_config.evm_with_env_and_inspector(self.db, env, &mut inspector); + let gas_used = { + let mut evm = eth_evm_config.evm_with_env_and_inspector(self.db.0, env, &mut inspector); + + // Execute the transaction. + let res = evm.transact().map_err(|err| TransactionError::Tracing(err.into()))?; - // Execute the transaction. - let res = transact_in_place(evm)?; + // Capture the gas used before `evm` goes out of scope. + res.result.gas_used() + }; // Get the call traces from the inspector. - let frame = inspector.into_geth_builder().geth_call_traces(call_config, res.result.gas_used()); + let frame = inspector.into_geth_builder().geth_call_traces(call_config, gas_used); // Return the obtained call traces. return Ok(frame.into()); @@ -321,10 +339,8 @@ impl Tracer

{ (TracingResult::default_failure(&self.tracing_options, tx), HashMap::default()) } else { match &self.tracing_options { - TracingOptions::Geth(opts) => Self::trace_geth(env, &mut db, tx, opts.clone())?, - TracingOptions::Parity(tracing_config) => { - Self::trace_parity(env, &mut db, tx, *tracing_config)? - } + TracingOptions::Geth(opts) => Self::trace_geth(env, &db, tx, opts.clone())?, + TracingOptions::Parity(tracing_config) => Self::trace_parity(env, &db, tx, *tracing_config)?, TracingOptions::GethCall(_) => { return Err(EthApiError::Transaction(TransactionError::Tracing( eyre!("`TracingOptions::GethCall` is not supported in `trace_transactions` context") @@ -340,7 +356,7 @@ impl Tracer

{ // Only commit to the database if there are more transactions to process. if transactions.peek().is_some() { - db.commit(state_changes); + db.0.commit(state_changes); } } @@ -361,30 +377,6 @@ fn env_with_tx(env: &EnvWithHandlerCfg, tx: reth_rpc_types::Transaction) -> Trac }) } -/// Runs the `evm.transact_commit()` in a blocking context using `tokio::task::block_in_place`. -/// This is needed in order to enter a blocking context which is then converted to a async -/// context in the implementation of [Database] using `Handle::current().block_on(async { ... })` -/// ⚠️ `evm.transact()` should NOT be used as is and we should always make use of the `transact_in_place` function -fn transact_in_place(mut evm: reth_revm::Evm<'_, I, DB>) -> TracerResult -where - ::Error: std::error::Error + Sync + Send + 'static, -{ - tokio::task::block_in_place(|| evm.transact().map_err(|err| TransactionError::Tracing(err.into()).into())) -} - -/// Runs the `evm.transact_commit()` in a blocking context using `tokio::task::block_in_place`. -/// This is needed in order to enter a blocking context which is then converted to a async -/// context in the implementation of [Database] using `Handle::current().block_on(async { ... })` -/// ⚠️ `evm.transact_commit()` should NOT be used as is and we should always make use of the `transaction_commit_in_place` function -fn transact_commit_in_place( - mut evm: reth_revm::Evm<'_, I, DB>, -) -> TracerResult -where - ::Error: std::error::Error + Sync + Send + 'static, -{ - tokio::task::block_in_place(|| evm.transact_commit().map_err(|err| TransactionError::Tracing(err.into()).into())) -} - #[cfg(test)] mod tests { use super::*; diff --git a/tests/tests/eth_provider.rs b/tests/tests/eth_provider.rs index 95722f4c8..19b7376b3 100644 --- a/tests/tests/eth_provider.rs +++ b/tests/tests/eth_provider.rs @@ -1,5 +1,6 @@ #![allow(clippy::used_underscore_binding)] #![cfg(feature = "testing")] +use alloy_sol_types::{sol, SolCall}; use kakarot_rpc::{ eth_provider::{ constant::{MAX_LOGS, STARKNET_MODULUS}, @@ -10,7 +11,7 @@ use kakarot_rpc::{ test_utils::{ eoa::Eoa, evm_contract::{EvmContract, KakarotEvmContract}, - fixtures::{contract_empty, counter, katana, setup}, + fixtures::{contract_empty, counter, katana, plain_opcodes, setup}, katana::Katana, mongo::{BLOCK_HASH, BLOCK_NUMBER}, tx_waiter::watch_tx, @@ -21,12 +22,12 @@ use reth_primitives::{ TxKind, TxLegacy, B256, U256, U64, }; use reth_rpc_types::{ - request::TransactionInput, serde_helpers::JsonStorageKey, Filter, FilterBlockOption, FilterChanges, Log, - RpcBlockHash, Topic, TransactionRequest, + request::TransactionInput, serde_helpers::JsonStorageKey, state::AccountOverride, Filter, FilterBlockOption, + FilterChanges, Log, RpcBlockHash, Topic, TransactionRequest, }; use rstest::*; use starknet::core::types::{BlockTag, Felt}; -use std::{str::FromStr, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; #[rstest] #[awt] @@ -796,6 +797,162 @@ async fn test_send_raw_transaction_wrong_signature(#[future] katana: Katana, _se assert!(tx.is_none()); } +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_call_without_overrides(#[future] katana: Katana, _setup: ()) { + // Obtain an Ethereum provider instance from the Katana instance + let eth_provider = katana.eth_provider(); + + // Get the EOA (Externally Owned Account) address from Katana + let eoa_address = katana.eoa().evm_address().expect("Failed to get eoa address"); + + // Create the first transaction request + let request1 = TransactionRequest { + from: Some(eoa_address), + to: Some(TxKind::Call(Address::ZERO)), + gas: Some(21000), + gas_price: Some(10), + value: Some(U256::from(1)), + ..Default::default() + }; + + // Perform the first call with state override and high balance + // The transaction should succeed + let _ = eth_provider.call(request1, None, None, None).await.expect("Failed to call for a simple transfer"); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_call_with_state_override_balance_success(#[future] katana: Katana, _setup: ()) { + // Obtain an Ethereum provider instance from the Katana instance + let eth_provider = katana.eth_provider(); + + // Generate an EOA address + let eoa_address = Address::from_str("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5").unwrap(); + + // Create the second transaction request with a higher value + let request = TransactionRequest { + from: Some(eoa_address), + to: Some(TxKind::Call(Address::ZERO)), + gas: Some(21000), + gas_price: Some(10), + value: Some(U256::from(1_000_000)), + ..Default::default() + }; + + // Initialize state override with the EOA address having a lower balance than the required value + let mut state_override: HashMap = HashMap::new(); + state_override + .insert(eoa_address, AccountOverride { balance: Some(U256::from(1_000_000_000)), ..Default::default() }); + + // Attempt to call and handle the result + // Should succeed as the EOA balance is higher than the required value + let _ = eth_provider + .call(request, None, Some(state_override.clone()), None) + .await + .expect("Failed to call for a simple transfer"); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_call_with_state_override_balance_failure(#[future] katana: Katana, _setup: ()) { + // Obtain an Ethereum provider instance from the Katana instance + let eth_provider = katana.eth_provider(); + + // Get the EOA (Externally Owned Account) address from Katana + let eoa_address = katana.eoa().evm_address().expect("Failed to get eoa address"); + + // Create the second transaction request with a higher value + let request = TransactionRequest { + from: Some(eoa_address), + to: Some(TxKind::Call(Address::ZERO)), + gas: Some(21000), + gas_price: Some(10), + value: Some(U256::from(1_000_000_001)), + ..Default::default() + }; + + // Initialize state override with the EOA address having a lower balance than the required value + let mut state_override: HashMap = HashMap::new(); + state_override + .insert(eoa_address, AccountOverride { balance: Some(U256::from(1_000_000_000)), ..Default::default() }); + + // Attempt to call and handle the result + let res = eth_provider.call(request, None, Some(state_override.clone()), None).await; + + // If the call succeeds, panic as an error was expected + // If the call fails, get the error and convert it to a string + let err = res.unwrap_err().to_string(); + + // Check if the error is due to insufficient funds + assert_eq!(err, "tracing error: transaction validation error: lack of funds (1000000000) for max fee (1000210001)"); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_call_with_state_override_bytecode(#[future] plain_opcodes: (Katana, KakarotEvmContract), _setup: ()) { + // Extract Katana instance from the plain_opcodes tuple + let katana = plain_opcodes.0; + + // Obtain an Ethereum provider instance from the Katana instance + let eth_provider = katana.eth_provider(); + + // Convert KakarotEvmContract's EVM address to an Address type + let contract_address = Address::from_slice(&plain_opcodes.1.evm_address.to_bytes_be()[12..]); + + // Get the EOA (Externally Owned Account) address from Katana + let eoa_address = katana.eoa().evm_address().expect("Failed to get eoa address"); + + // Define another Solidity contract with a different interface and bytecode + sol! { + #[sol(rpc, bytecode = "6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033")] + #[derive(Debug)] + contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } + } + } + + // Extract the bytecode for the counter contract + let bytecode = &Counter::BYTECODE[..]; + + // Prepare the calldata for invoking the setNumber function + let calldata = Counter::setNumberCall { newNumber: U256::from(10) }.abi_encode(); + + // State override with the Counter bytecode + let mut state_override: HashMap = HashMap::new(); + state_override.insert(contract_address, AccountOverride { code: Some(bytecode.into()), ..Default::default() }); + + // Define the transaction request for invoking the setNumber function + let request = TransactionRequest { + from: Some(eoa_address), + to: Some(TxKind::Call(contract_address)), + gas: Some(210_000), + gas_price: Some(1_000_000_000_000_000_000_000), + value: Some(U256::ZERO), + nonce: Some(2), + input: TransactionInput { input: Some(calldata.clone().into()), data: None }, + ..Default::default() + }; + + // Attempt to call the setNumber function and handle the result + let _ = eth_provider + .call(request, None, Some(state_override), None) + .await + .expect("Failed to set number in Counter contract"); +} + #[rstest] #[awt] #[tokio::test(flavor = "multi_thread")]