Skip to content

Commit

Permalink
add state and block overrides in eth_call (kkrt-labs#1299)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tcoratger authored Jul 24, 2024
1 parent 963dea9 commit 305c68f
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 195 deletions.
2 changes: 2 additions & 0 deletions src/eth_provider/contracts/erc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ impl<P: EthereumProvider> EthereumErc20<P> {
..Default::default()
},
Some(block_id),
None,
None,
)
.await
}
Expand Down
1 change: 1 addition & 0 deletions src/eth_provider/database/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod ethereum;
pub mod filter;
pub mod state;
pub mod types;

use super::error::KakarotError;
Expand Down
107 changes: 107 additions & 0 deletions src/eth_provider/database/state.rs
Original file line number Diff line number Diff line change
@@ -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<P: EthereumProvider + Send + Sync>(pub CacheDB<EthDatabase<P>>);

/// Ethereum database type.
#[derive(Debug, Clone)]
#[allow(clippy::redundant_pub_crate)]
pub struct EthDatabase<P: EthereumProvider + Send + Sync> {
/// The Ethereum provider.
provider: P,
/// The block ID.
block_id: BlockId,
}

impl<P: EthereumProvider + Send + Sync> EthDatabase<P> {
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<P: EthereumProvider + Send + Sync> DatabaseRef for EthDatabase<P> {
type Error = EthApiError;

/// Returns the account information for the given address without caching.
fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, 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<Bytecode, Self::Error> {
Ok(Default::default())
}

/// Returns the storage value for the given address and index without caching.
fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
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<B256, Self::Error> {
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)
})
}
}
32 changes: 28 additions & 4 deletions src/eth_provider/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -44,6 +45,12 @@ impl From<&EthApiError> for EthRpcErrorCode {
}
}

impl From<EthApiError> 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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -98,16 +105,29 @@ impl std::fmt::Display for EthApiError {
/// Constructs a JSON-RPC error object, consisting of `code` and `message`.
impl From<EthApiError> 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.
Expand Down Expand Up @@ -286,6 +306,9 @@ pub enum TransactionError {
/// Thrown if the tracing fails
#[error("tracing error: {0}")]
Tracing(Box<dyn std::error::Error + Send + Sync>),
/// Thrown if the call with state or block overrides fails
#[error("tracing error: {0}")]
Call(Box<dyn std::error::Error + Send + Sync>),
}

impl From<&TransactionError> for EthRpcErrorCode {
Expand All @@ -297,6 +320,7 @@ impl From<&TransactionError> for EthRpcErrorCode {
| TransactionError::TipAboveFeeCap(_, _) => Self::TransactionRejected,
TransactionError::ExpectedFullTransactions
| TransactionError::Tracing(_)
| TransactionError::Call(_)
| TransactionError::ExceedsBlockGasLimit(_, _) => Self::InternalError,
}
}
Expand Down
88 changes: 83 additions & 5 deletions src/eth_provider/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::{
database::{
ethereum::EthereumBlockStore,
filter::EthDatabaseFilterBuilder,
state::{EthCacheDatabase, EthDatabase},
types::{
header::StoredHeader,
log::StoredLog,
Expand All @@ -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},
Expand All @@ -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")]
Expand Down Expand Up @@ -119,7 +134,13 @@ pub trait EthereumProvider {
/// Returns the logs for the given filter.
async fn get_logs(&self, filter: Filter) -> EthProviderResult<FilterChanges>;
/// Returns the result of a call.
async fn call(&self, request: TransactionRequest, block_id: Option<BlockId>) -> EthProviderResult<Bytes>;
async fn call(
&self,
request: TransactionRequest,
block_id: Option<BlockId>,
state_overrides: Option<StateOverride>,
block_overrides: Option<Box<BlockOverrides>>,
) -> EthProviderResult<Bytes>;
/// Returns the result of a estimate gas.
async fn estimate_gas(&self, call: TransactionRequest, block_id: Option<BlockId>) -> EthProviderResult<U256>;
/// Returns the fee history given a block count and a newest block number.
Expand Down Expand Up @@ -450,7 +471,64 @@ where
))
}

async fn call(&self, request: TransactionRequest, block_id: Option<BlockId>) -> EthProviderResult<Bytes> {
async fn call(
&self,
request: TransactionRequest,
block_id: Option<BlockId>,
state_overrides: Option<StateOverride>,
block_overrides: Option<Box<BlockOverrides>>,
) -> EthProviderResult<Bytes> {
// 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| <TransactionError as Into<EthApiError>>::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::<Vec<_>>()))
}
Expand Down
14 changes: 10 additions & 4 deletions src/eth_rpc/api/eth_api.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<BlockId>) -> Result<Bytes>;
async fn call(
&self,
request: TransactionRequest,
block_id: Option<BlockId>,
state_overrides: Option<StateOverride>,
block_overrides: Option<Box<BlockOverrides>>,
) -> Result<Bytes>;

/// Generates an access list for a transaction.
///
Expand Down
15 changes: 11 additions & 4 deletions src/eth_rpc/servers/eth_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -154,8 +155,14 @@ where
}

#[tracing::instrument(skip(self, request), err)]
async fn call(&self, request: TransactionRequest, block_id: Option<BlockId>) -> Result<Bytes> {
Ok(self.eth_provider.call(request, block_id).await?)
async fn call(
&self,
request: TransactionRequest,
block_id: Option<BlockId>,
state_overrides: Option<StateOverride>,
block_overrides: Option<Box<BlockOverrides>>,
) -> Result<Bytes> {
Ok(self.eth_provider.call(request, block_id, state_overrides, block_overrides).await?)
}

async fn create_access_list(
Expand Down
Loading

0 comments on commit 305c68f

Please sign in to comment.