Skip to content

Commit

Permalink
RFC: Add rpc method eth_callMany (#4070)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
libevm and mattsse authored Aug 5, 2023
1 parent 3aff8de commit 5298868
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 7 deletions.
15 changes: 13 additions & 2 deletions crates/rpc/rpc-api/src/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use reth_primitives::{
AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, H256, H64, U256, U64,
};
use reth_rpc_types::{
state::StateOverride, BlockOverrides, CallRequest, EIP1186AccountProofResponse, FeeHistory,
Index, RichBlock, SyncStatus, Transaction, TransactionReceipt, TransactionRequest, Work,
state::StateOverride, BlockOverrides, Bundle, CallRequest, EIP1186AccountProofResponse,
EthCallResponse, FeeHistory, Index, RichBlock, StateContext, SyncStatus, Transaction,
TransactionReceipt, TransactionRequest, Work,
};

/// Eth rpc interface: <https://ethereum.github.io/execution-apis/api-documentation/>
Expand Down Expand Up @@ -153,6 +154,16 @@ pub trait EthApi {
block_overrides: Option<Box<BlockOverrides>>,
) -> RpcResult<Bytes>;

/// Simulate arbitrary number of transactions at an arbitrary blockchain index, with the
/// optionality of state overrides
#[method(name = "callMany")]
async fn call_many(
&self,
bundle: Bundle,
state_context: Option<StateContext>,
state_override: Option<StateOverride>,
) -> RpcResult<Vec<EthCallResponse>>;

/// Generates an access list for a transaction.
///
/// This method creates an [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) type accessList based on a given Transaction.
Expand Down
12 changes: 12 additions & 0 deletions crates/rpc/rpc-types/src/eth/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ pub struct StateContext {
pub transaction_index: Option<TransactionIndex>,
}

/// CallResponse for eth_callMany
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct EthCallResponse {
#[serde(skip_serializing_if = "Option::is_none")]
/// eth_call output (if no error)
pub output: Option<Bytes>,
#[serde(skip_serializing_if = "Option::is_none")]
/// eth_call output (if error)
pub error: Option<String>,
}

/// Represents a transaction index where -1 means all transactions
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum TransactionIndex {
Expand Down
2 changes: 1 addition & 1 deletion crates/rpc/rpc-types/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod work;

pub use account::*;
pub use block::*;
pub use call::{Bundle, CallInput, CallInputError, CallRequest, StateContext};
pub use call::{Bundle, CallInput, CallInputError, CallRequest, EthCallResponse, StateContext};
pub use fee::{FeeHistory, TxGasAndReward};
pub use filter::*;
pub use index::Index;
Expand Down
97 changes: 95 additions & 2 deletions crates/rpc/rpc/src/eth/api/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
error::{ensure_success, EthApiError, EthResult, RevertError, RpcInvalidTransactionError},
revm_utils::{
build_call_evm_env, caller_gas_allowance, cap_tx_gas_limit_with_caller_allowance,
get_precompiles, inspect, transact, EvmOverrides,
get_precompiles, inspect, prepare_call_env, transact, EvmOverrides,
},
EthTransactions,
},
Expand All @@ -18,12 +18,16 @@ use reth_provider::{BlockReaderIdExt, EvmEnvProvider, StateProvider, StateProvid
use reth_revm::{
access_list::AccessListInspector,
database::{State, SubState},
env::tx_env_with_recovered,
};
use reth_rpc_types::{
state::StateOverride, BlockError, Bundle, CallRequest, EthCallResponse, StateContext,
};
use reth_rpc_types::CallRequest;
use reth_transaction_pool::TransactionPool;
use revm::{
db::{CacheDB, DatabaseRef},
primitives::{BlockEnv, CfgEnv, Env, ExecutionResult, Halt, TransactTo},
DatabaseCommit,
};
use tracing::trace;

Expand Down Expand Up @@ -62,6 +66,95 @@ where
ensure_success(res.result)
}

/// Simulate arbitrary number of transactions at an arbitrary blockchain index, with the
/// optionality of state overrides
pub async fn call_many(
&self,
bundle: Bundle,
state_context: Option<StateContext>,
state_override: Option<StateOverride>,
) -> EthResult<Vec<EthCallResponse>> {
let Bundle { transactions, block_override } = bundle;
if transactions.is_empty() {
return Err(EthApiError::InvalidParams(String::from("transactions are empty.")))
}

let StateContext { transaction_index, block_number } = state_context.unwrap_or_default();
let transaction_index = transaction_index.unwrap_or_default();

let target_block = block_number.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest));
let ((cfg, block_env, _), block) =
futures::try_join!(self.evm_env_at(target_block), self.block_by_id(target_block))?;

let block = block.ok_or_else(|| EthApiError::UnknownBlockNumber)?;
let gas_limit = self.inner.gas_cap;

// we're essentially replaying the transactions in the block here, hence we need the state
// that points to the beginning of the block, which is the state at the parent block
let mut at = block.parent_hash;
let mut replay_block_txs = true;

// but if all transactions are to be replayed, we can use the state at the block itself
let num_txs = transaction_index.index().unwrap_or(block.body.len());
if num_txs == block.body.len() {
at = block.hash;
replay_block_txs = false;
}

self.spawn_with_state_at_block(at.into(), move |state| {
let mut results = Vec::with_capacity(transactions.len());
let mut db = SubState::new(State::new(state));

if replay_block_txs {
// only need to replay the transactions in the block if not all transactions are
// to be replayed
let transactions = block.body.into_iter().take(num_txs);

// Execute all transactions until index
for tx in transactions {
let tx = tx.into_ecrecovered().ok_or(BlockError::InvalidSignature)?;
let tx = tx_env_with_recovered(&tx);
let env = Env { cfg: cfg.clone(), block: block_env.clone(), tx };
let (res, _) = transact(&mut db, env)?;
db.commit(res.state);
}
}

let overrides = EvmOverrides::new(state_override.clone(), block_override.map(Box::new));

let mut transactions = transactions.into_iter().peekable();
while let Some(tx) = transactions.next() {
let env = prepare_call_env(
cfg.clone(),
block_env.clone(),
tx,
gas_limit,
&mut db,
overrides.clone(),
)?;
let (res, _) = transact(&mut db, env)?;

match ensure_success(res.result) {
Ok(output) => {
results.push(EthCallResponse { output: Some(output), error: None });
}
Err(err) => {
results
.push(EthCallResponse { output: None, error: Some(err.to_string()) });
}
}

if transactions.peek().is_some() {
// need to apply the state changes of this call before executing the next call
db.commit(res.state);
}
}

Ok(results)
})
.await
}

/// Estimates the gas usage of the `request` with the state.
///
/// This will execute the [CallRequest] and find the best gas limit via binary search
Expand Down
16 changes: 14 additions & 2 deletions crates/rpc/rpc/src/eth/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use reth_provider::{
};
use reth_rpc_api::EthApiServer;
use reth_rpc_types::{
state::StateOverride, BlockOverrides, CallRequest, EIP1186AccountProofResponse, FeeHistory,
Index, RichBlock, SyncStatus, TransactionReceipt, TransactionRequest, Work,
state::StateOverride, BlockOverrides, Bundle, CallRequest, EIP1186AccountProofResponse,
EthCallResponse, FeeHistory, Index, RichBlock, StateContext, SyncStatus, TransactionReceipt,
TransactionRequest, Work,
};
use reth_transaction_pool::TransactionPool;
use serde_json::Value;
Expand Down Expand Up @@ -245,6 +246,17 @@ where
.await?)
}

/// Handler for: `eth_callMany`
async fn call_many(
&self,
bundle: Bundle,
state_context: Option<StateContext>,
state_override: Option<StateOverride>,
) -> Result<Vec<EthCallResponse>> {
trace!(target: "rpc::eth", ?bundle, ?state_context, ?state_override, "Serving eth_callMany");
Ok(EthApi::call_many(self, bundle, state_context, state_override).await?)
}

/// Handler for: `eth_createAccessList`
async fn create_access_list(
&self,
Expand Down

0 comments on commit 5298868

Please sign in to comment.