From 0e9d18d4e923dc9f972c8c6eee46da489a17ce48 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 4 Mar 2023 21:44:33 +0100 Subject: [PATCH] feat(rpc): support eth_call state overrides (#1634) --- crates/rpc/rpc-api/src/eth.rs | 11 ++- crates/rpc/rpc-builder/tests/it/http.rs | 2 +- crates/rpc/rpc-types/src/eth/state.rs | 2 +- crates/rpc/rpc/src/eth/api/call.rs | 89 +++++++++++++++++++++++-- crates/rpc/rpc/src/eth/api/server.rs | 12 +++- crates/rpc/rpc/src/eth/error.rs | 6 +- 6 files changed, 109 insertions(+), 13 deletions(-) diff --git a/crates/rpc/rpc-api/src/eth.rs b/crates/rpc/rpc-api/src/eth.rs index 5b6553e731..b16cf6bf0f 100644 --- a/crates/rpc/rpc-api/src/eth.rs +++ b/crates/rpc/rpc-api/src/eth.rs @@ -4,8 +4,8 @@ use reth_primitives::{ H256, H64, U256, U64, }; use reth_rpc_types::{ - CallRequest, EIP1186AccountProofResponse, FeeHistory, Index, RichBlock, SyncStatus, - Transaction, TransactionReceipt, TransactionRequest, Work, + state::StateOverride, CallRequest, EIP1186AccountProofResponse, FeeHistory, Index, RichBlock, + SyncStatus, Transaction, TransactionReceipt, TransactionRequest, Work, }; /// Eth rpc interface: @@ -135,7 +135,12 @@ pub trait EthApi { /// Executes a new message call immediately without creating a transaction on the block chain. #[method(name = "eth_call")] - async fn call(&self, request: CallRequest, block_number: Option) -> Result; + async fn call( + &self, + request: CallRequest, + block_number: Option, + state_overrides: Option, + ) -> Result; /// Generates an access list for a transaction. /// diff --git a/crates/rpc/rpc-builder/tests/it/http.rs b/crates/rpc/rpc-builder/tests/it/http.rs index f6eec08ba3..c709f9d079 100644 --- a/crates/rpc/rpc-builder/tests/it/http.rs +++ b/crates/rpc/rpc-builder/tests/it/http.rs @@ -98,7 +98,7 @@ where )); assert!(is_unimplemented(EthApiClient::transaction_receipt(client, hash).await.err().unwrap())); assert!(is_unimplemented( - EthApiClient::call(client, call_request.clone(), None).await.err().unwrap() + EthApiClient::call(client, call_request.clone(), None, None).await.err().unwrap() )); assert!(is_unimplemented( EthApiClient::create_access_list(client, call_request.clone(), None).await.err().unwrap() diff --git a/crates/rpc/rpc-types/src/eth/state.rs b/crates/rpc/rpc-types/src/eth/state.rs index c2d1edadce..9fbc0d8a13 100644 --- a/crates/rpc/rpc-types/src/eth/state.rs +++ b/crates/rpc/rpc-types/src/eth/state.rs @@ -9,7 +9,7 @@ pub type StateOverride = HashMap; /// Custom account override used in call #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] #[allow(missing_docs)] pub struct AccountOverride { pub nonce: Option, diff --git a/crates/rpc/rpc/src/eth/api/call.rs b/crates/rpc/rpc/src/eth/api/call.rs index 81414db12a..7c8ffc7069 100644 --- a/crates/rpc/rpc/src/eth/api/call.rs +++ b/crates/rpc/rpc/src/eth/api/call.rs @@ -11,11 +11,15 @@ use reth_primitives::{ }; use reth_provider::{BlockProvider, EvmEnvProvider, StateProvider, StateProviderFactory}; use reth_revm::database::{State, SubState}; -use reth_rpc_types::CallRequest; +use reth_rpc_types::{ + state::{AccountOverride, StateOverride}, + CallRequest, +}; use revm::{ + db::{CacheDB, DatabaseRef}, primitives::{ - ruint::Uint, BlockEnv, CfgEnv, Env, ExecutionResult, Halt, ResultAndState, TransactTo, - TxEnv, + ruint::Uint, BlockEnv, Bytecode, CfgEnv, Env, ExecutionResult, Halt, ResultAndState, + TransactTo, TxEnv, }, Database, }; @@ -55,10 +59,11 @@ where &self, request: CallRequest, at: BlockId, + state_overrides: Option, ) -> EthResult<(ResultAndState, Env)> { let (cfg, block_env, at) = self.evm_env_at(at).await?; let state = self.state_at_block_id(at)?.ok_or_else(|| EthApiError::UnknownBlockNumber)?; - self.call_with(cfg, block_env, request, state) + self.call_with(cfg, block_env, request, state, state_overrides) } /// Executes the call request using the given environment against the state provider @@ -70,6 +75,7 @@ where block: BlockEnv, mut request: CallRequest, state: S, + state_overrides: Option, ) -> EthResult<(ResultAndState, Env)> where S: StateProvider, @@ -80,6 +86,12 @@ where let mut env = build_call_evm_env(cfg, block, request)?; let mut db = SubState::new(State::new(state)); + + // apply state overrides + if let Some(state_overrides) = state_overrides { + apply_state_overrides(state_overrides, &mut db)?; + } + transact(&mut db, env) } @@ -391,3 +403,72 @@ impl CallFees { } } } + +/// Applies the given state overrides (a set of [AccountOverride]) to the [CacheDB]. +fn apply_state_overrides(overrides: StateOverride, db: &mut CacheDB) -> EthResult<()> +where + DB: DatabaseRef, + EthApiError: From<::Error>, +{ + for (account, account_overrides) in overrides { + apply_account_override(account, account_overrides, db)?; + } + Ok(()) +} + +/// Applies a single [AccountOverride] to the [CacheDB]. +fn apply_account_override( + account: Address, + account_override: AccountOverride, + db: &mut CacheDB, +) -> EthResult<()> +where + DB: DatabaseRef, + EthApiError: From<::Error>, +{ + let mut account_info = db.basic(account)?.unwrap_or_default(); + + if let Some(nonce) = account_override.nonce { + account_info.nonce = nonce; + } + if let Some(code) = account_override.code { + account_info.code = Some(Bytecode::new_raw(code.0)); + } + if let Some(balance) = account_override.balance { + account_info.balance = balance; + } + + db.insert_account_info(account, account_info); + + // We ensure that not both state and state_diff are set. + // If state is set, we must mark the account as "NewlyCreated", so that the old storage + // isn't read from + match (account_override.state, account_override.state_diff) { + (Some(_), Some(_)) => return Err(EthApiError::BothStateAndStateDiffInOverride(account)), + (None, None) => { + // nothing to do + } + (Some(new_account_state), None) => { + db.replace_account_storage( + account, + new_account_state + .into_iter() + .map(|(slot, value)| { + (U256::from_be_bytes(slot.0), U256::from_be_bytes(value.0)) + }) + .collect(), + )?; + } + (None, Some(account_state_diff)) => { + for (slot, value) in account_state_diff { + db.insert_account_storage( + account, + U256::from_be_bytes(slot.0), + U256::from_be_bytes(value.0), + )?; + } + } + }; + + Ok(()) +} diff --git a/crates/rpc/rpc/src/eth/api/server.rs b/crates/rpc/rpc/src/eth/api/server.rs index ea8afb3556..471ff2eb16 100644 --- a/crates/rpc/rpc/src/eth/api/server.rs +++ b/crates/rpc/rpc/src/eth/api/server.rs @@ -14,8 +14,9 @@ use reth_primitives::{ use reth_provider::{BlockProvider, EvmEnvProvider, HeaderProvider, StateProviderFactory}; use reth_rpc_api::EthApiServer; use reth_rpc_types::{ - CallRequest, EIP1186AccountProofResponse, FeeHistory, FeeHistoryCacheItem, Index, RichBlock, - SyncStatus, TransactionReceipt, TransactionRequest, Work, + state::StateOverride, CallRequest, EIP1186AccountProofResponse, FeeHistory, + FeeHistoryCacheItem, Index, RichBlock, SyncStatus, TransactionReceipt, TransactionRequest, + Work, }; use reth_transaction_pool::TransactionPool; use serde_json::Value; @@ -177,7 +178,12 @@ where } /// Handler for: `eth_call` - async fn call(&self, _request: CallRequest, _block_number: Option) -> Result { + async fn call( + &self, + _request: CallRequest, + _block_number: Option, + _state_overrides: Option, + ) -> Result { Err(internal_rpc_err("unimplemented")) } diff --git a/crates/rpc/rpc/src/eth/error.rs b/crates/rpc/rpc/src/eth/error.rs index ee1e42ab0e..8a080c7b52 100644 --- a/crates/rpc/rpc/src/eth/error.rs +++ b/crates/rpc/rpc/src/eth/error.rs @@ -2,7 +2,7 @@ use crate::result::{internal_rpc_err, rpc_err}; use jsonrpsee::{core::Error as RpcError, types::error::INVALID_PARAMS_CODE}; -use reth_primitives::{constants::SELECTOR_LEN, U128, U256}; +use reth_primitives::{constants::SELECTOR_LEN, Address, U128, U256}; use reth_rpc_types::BlockError; use reth_transaction_pool::error::PoolError; use revm::primitives::{EVMError, Halt}; @@ -67,6 +67,10 @@ pub(crate) enum EthApiError { /// Thrown when constructing an RPC block from a primitive block data failed. #[error(transparent)] InvalidBlockData(#[from] BlockError), + /// Thrown when a [AccountOverride](reth_rpc_types::state::AccountOverride) contains + /// conflicting `state` and `stateDiff` fields + #[error("account {0:?} has both 'state' and 'stateDiff'")] + BothStateAndStateDiffInOverride(Address), /// Other internal error #[error(transparent)] Internal(#[from] reth_interfaces::Error),