Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anvil): add eth_simulateV1 rpc call #10030

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use alloy_rpc_types::{
anvil::{Forking, MineOptions},
pubsub::{Params as SubscriptionParams, SubscriptionKind},
request::TransactionRequest,
simulate::SimulatePayload,
state::StateOverride,
trace::{
filter::TraceFilter,
Expand Down Expand Up @@ -184,6 +185,9 @@ pub enum EthRequest {
#[cfg_attr(feature = "serde", serde(default))] Option<StateOverride>,
),

#[cfg_attr(feature = "serde", serde(rename = "eth_simulateV1"))]
EthSimulateV1(SimulatePayload, #[cfg_attr(feature = "serde", serde(default))] Option<BlockId>),

#[cfg_attr(feature = "serde", serde(rename = "eth_createAccessList"))]
EthCreateAccessList(
WithOtherFields<TransactionRequest>,
Expand Down
43 changes: 43 additions & 0 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use crate::{
use alloy_consensus::{
transaction::{eip4844::TxEip4844Variant, Recovered},
Account,
Header,
};
use alloy_dyn_abi::TypedData;
use alloy_eips::eip2718::Encodable2718;
Expand All @@ -59,6 +60,7 @@ use alloy_rpc_types::{
geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace},
parity::LocalizedTransactionTrace,
},
simulate::{SimulatePayload, SimulatedBlock},
txpool::{TxpoolContent, TxpoolInspect, TxpoolInspectSummary, TxpoolStatus},
AccessList, AccessListResult, BlockId, BlockNumberOrTag as BlockNumber, BlockTransactions,
EIP1186AccountProofResponse, FeeHistory, Filter, FilteredParams, Index, Log,
Expand Down Expand Up @@ -93,9 +95,19 @@ use parking_lot::RwLock;
use revm::primitives::Bytecode;
use std::{future::Future, sync::Arc, time::Duration};

use serde::Serialize;

/// The client version: `anvil/v{major}.{minor}.{patch}`
pub const CLIENT_VERSION: &str = concat!("anvil/v", env!("CARGO_PKG_VERSION"));

// TODO: I feel like this enum should go somewhere more proper. Where would be best?
#[derive(Serialize)]
#[serde(untagged)]
pub enum SimulatedBlockResponse {
AnvilInternal(Vec<SimulatedBlock<alloy_rpc_types::Block<alloy_rpc_types::Transaction, Header>>>),
Forked(Vec<SimulatedBlock<AnyRpcBlock>>)
}

/// The entry point for executing eth api RPC call - The Eth RPC interface.
///
/// This type is cheap to clone and can be used concurrently
Expand Down Expand Up @@ -249,6 +261,9 @@ impl EthApi {
EthRequest::EthCall(call, block, overrides) => {
self.call(call, block, overrides).await.to_rpc_result()
}
EthRequest::EthSimulateV1(simulation, block) => {
self.simulate_v1(simulation, block).await.to_rpc_result()
}
EthRequest::EthCreateAccessList(call, block) => {
self.create_access_list(call, block).await.to_rpc_result()
}
Expand Down Expand Up @@ -1103,6 +1118,34 @@ impl EthApi {
.await
}

pub async fn simulate_v1(
&self,
request: SimulatePayload,
block_number: Option<BlockId>,
) -> Result<SimulatedBlockResponse> {
node_info!("eth_simulateV1");
let block_request = self.block_request(block_number).await?;
// check if the number predates the fork, if in fork mode
if let BlockRequest::Number(number) = block_request {
if let Some(fork) = self.get_fork() {
if fork.predates_fork(number) {
return Ok(SimulatedBlockResponse::Forked(fork.simulate_v1(&request, Some(number.into())).await?))
}
}
}

// this can be blocking for a bit, especially in forking mode
// <https://github.com/foundry-rs/foundry/issues/6036>
self.on_blocking_task(|this| async move {
let simulated_blocks =
this.backend.simulate(request, Some(block_request)).await?;
trace!(target : "node", "Simulate status {:?}", simulated_blocks);

Ok(SimulatedBlockResponse::AnvilInternal(simulated_blocks))
})
.await
}

/// This method creates an EIP2930 type accessList based on a given Transaction. The accessList
/// contains all storage slots and addresses read and written by the transaction, except for the
/// sender account and the precompiles.
Expand Down
19 changes: 18 additions & 1 deletion crates/anvil/src/eth/backend/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use alloy_provider::{
};
use alloy_rpc_types::{
request::TransactionRequest,
simulate::{SimulatePayload, SimulatedBlock},
trace::{
geth::{GethDebugTracingOptions, GethTrace},
parity::LocalizedTransactionTrace as Trace,
Expand Down Expand Up @@ -197,7 +198,23 @@ impl ClientFork {
Ok(res)
}

/// Sends `eth_call`
/// Sends `eth_simulateV1`
pub async fn simulate_v1(
&self,
request: &SimulatePayload,
block: Option<BlockNumber>,
) -> Result<Vec<SimulatedBlock<AnyRpcBlock>>, TransportError> {
let mut simulate_call = self.provider().simulate(request);
if let Some(n) = block {
simulate_call = simulate_call.number(n.as_number().unwrap());
}

let res = simulate_call.await?;

Ok(res)
}

/// Sends `eth_estimateGas`
pub async fn estimate_gas(
&self,
request: &WithOtherFields<TransactionRequest>,
Expand Down
142 changes: 138 additions & 4 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use self::state::trie_storage;
use super::executor::new_evm_with_inspector_ref;
use super::super::sign::build_typed_transaction;
use crate::{
config::PruneStateHistoryConfig,
eth::{
Expand All @@ -22,7 +23,7 @@ use crate::{
error::{BlockchainError, ErrDetail, InvalidTransactionError},
fees::{FeeDetails, FeeManager, MIN_SUGGESTED_PRIORITY_FEE},
macros::node_info,
pool::transactions::PoolTransaction,
pool::transactions::{PoolTransaction, TransactionPriority},
util::get_precompiles_for,
},
inject_precompiles,
Expand All @@ -36,20 +37,21 @@ use crate::{
use alloy_chains::NamedChain;
use alloy_consensus::{
transaction::Recovered, Account, Header, Receipt, ReceiptWithBloom, Signed,
Transaction as TransactionTrait, TxEnvelope,
Transaction as TransactionTrait, TxEip1559, TxEnvelope,
};
use alloy_eips::eip4844::MAX_BLOBS_PER_BLOCK;
use alloy_network::{
AnyHeader, AnyRpcBlock, AnyRpcTransaction, AnyTxEnvelope, AnyTxType, EthereumWallet,
UnknownTxEnvelope, UnknownTypedTransaction,
};
use alloy_primitives::{
address, hex, keccak256, utils::Unit, Address, Bytes, TxHash, TxKind, B256, U256, U64,
address, hex, keccak256, utils::Unit, Address, Bytes, TxHash, TxKind, PrimitiveSignature, B256, U256, U64,
};
use alloy_rpc_types::{
anvil::Forking,
request::TransactionRequest,
serde_helpers::JsonStorageKey,
simulate::{SimulatePayload, SimulatedBlock, SimCallResult},
state::StateOverride,
trace::{
filter::TraceFilter,
Expand All @@ -70,7 +72,7 @@ use anvil_core::eth::{
block::{Block, BlockInfo},
transaction::{
optimism::DepositTransaction, DepositReceipt, MaybeImpersonatedTransaction,
PendingTransaction, ReceiptResponse, TransactionInfo, TypedReceipt, TypedTransaction,
PendingTransaction, ReceiptResponse, TransactionInfo, TypedReceipt, TypedTransaction, TypedTransactionRequest
},
wallet::{Capabilities, DelegationCapability, WalletCapabilities},
};
Expand Down Expand Up @@ -1454,6 +1456,138 @@ impl Backend {
inspector
}

pub async fn simulate(
&self,
request: SimulatePayload,
block_request: Option<BlockRequest>,
) -> Result<Vec<SimulatedBlock<alloy_rpc_types::Block<Transaction, alloy_consensus::Header>>>, BlockchainError> {
// first, snapshot the current state
let state_id = self.db.write().await.snapshot_state();

// set zero address to have a balance so that read calls dont fail
// (this seems to be part of the standard)
self.set_balance(Address::ZERO, U256::MAX).await?;

// then, execute each block in the way that it is given
let mut simulated_blocks = Vec::new();
for block in request.block_state_calls {
// if the next block number is larger than the current next block height,
// we should mint an empty block and continue
if let Some(overrides) = block.block_overrides {

if let Some(needed_number) = overrides.number {
let best_number = self.best_number();
while
best_number < needed_number.to() {

// mint an empty block per spec
self.do_mine_block(Vec::new()).await;
// TODO: add simulated block
if simulated_blocks.len() > 256 {
// exceeded block simulation limit
return Err(RpcError::internal_error_with("exceeded block simulation limit").into());
}
}
}
}

if let Some(state_overrides) = block.state_overrides {
for (addr, addr_overrides) in state_overrides {
if let Some(new_balance) = addr_overrides.balance {
self.set_balance(addr, new_balance).await?;
}
if let Some(new_code) = addr_overrides.code {
self.set_code(addr, new_code).await?;
}
if let Some(new_nonce) = addr_overrides.nonce {
self.set_nonce(addr, U256::from(new_nonce)).await?;
}
if let Some(new_state) = addr_overrides.state {
for (k,v) in new_state {
self.set_storage_at(addr, k.into(), v).await?;
}
}
}
}

let pool_transactions = block.calls.iter().map(|t| Arc::new(PoolTransaction {
pending_transaction: PendingTransaction::with_impersonated(
build_typed_transaction(TypedTransactionRequest::EIP1559(TxEip1559 {
nonce: t.nonce.unwrap_or_default(),
// TODO: how to do gwei conversion? the number below
// for now should be 100 gwei
max_fee_per_gas: t.max_fee_per_gas.unwrap_or(100000000000),
max_priority_fee_per_gas: t.max_priority_fee_per_gas.unwrap_or_default(),
// TODO: seems like it would be best to estimate the
// amount of gas needed here if not provided? or
// otherwise, not bound the block by the total required
// gas. for now I set to 1 million for testing
gas_limit: t.gas.unwrap_or(1000000),
value: t.value.unwrap_or_default(),
input: t.input.clone().into_input().unwrap_or_default(),
to: t.to.unwrap_or_default(),
chain_id: self.env.read().cfg.chain_id,
access_list: t.access_list.clone().unwrap_or_default(),
}), PrimitiveSignature::from_scalars_and_parity(
B256::with_last_byte(1),
B256::with_last_byte(1),
false,
)).unwrap(),
t.from.unwrap_or(Address::default())
),
requires: Vec::new(),
provides: Vec::new(),
priority: TransactionPriority(0)
// TODO: priority is by highest first, so just doing a sort of reverse here
//priority: TransactionPriority((1000000000 - i).try_into().unwrap())
})).collect();

// TODO: it may be possible to be more lightweight with our handling of blot.ck mining
// here, but for now this should be pretty foolproof (in theory)
let outcome = self.do_mine_block(pool_transactions).await;
if outcome.invalid.len() > 0 {
return Err(RpcError::internal_error_with("txn was invalid").into());
}

let executed_block = self.get_block(outcome.block_number).unwrap();

let mut simulated_block = SimulatedBlock {
inner: alloy_rpc_types::Block { header: executed_block.header, transactions: BlockTransactions::Hashes(outcome.included.iter().map(|t| t.hash()).collect()), uncles: Vec::new(), withdrawals: Some(alloy_rpc_types::Withdrawals(Vec::new())) },
calls: Vec::new()
};

for tx in outcome.included {
let MinedTransaction { info, receipt: raw_receipt, .. } =
self.blockchain.get_transaction_by_hash(&tx.hash()).unwrap();
let receipt = raw_receipt.as_receipt_with_bloom().receipt.clone();

let status = match receipt.status { alloy_consensus::Eip658Value::Eip658(true) => true, _ => false};

simulated_block.calls.push(SimCallResult {
return_data: info.out.unwrap_or_default(),
// TODO: fill actual error
error: match status { false => Some(alloy_rpc_types::simulate::SimulateError { code: -3200, message: "execution failed".to_string() }), true => None },
gas_used: receipt.cumulative_gas_used,
// TODO: fill actual logs
logs: Vec::new(),
status
});
}

simulated_blocks.push(simulated_block);

if simulated_blocks.len() >= 256 {
// exceeded block simulation limit
return Err(RpcError::internal_error_with("exceeded block simulation limit").into());
}
}

// finally, revert back to the previous state
self.db.write().await.revert_state(state_id, RevertStateSnapshotAction::RevertRemove);

Ok(simulated_blocks)
}

pub fn call_with_state(
&self,
state: &dyn DatabaseRef<Error = DatabaseError>,
Expand Down
Loading