diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index e9a46611e7f3a..260f04d5329dc 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -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, @@ -184,6 +185,9 @@ pub enum EthRequest { #[cfg_attr(feature = "serde", serde(default))] Option, ), + #[cfg_attr(feature = "serde", serde(rename = "eth_simulateV1"))] + EthSimulateV1(SimulatePayload, #[cfg_attr(feature = "serde", serde(default))] Option), + #[cfg_attr(feature = "serde", serde(rename = "eth_createAccessList"))] EthCreateAccessList( WithOtherFields, diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 66405f1d455fa..dbb5db14a4399 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -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; @@ -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, @@ -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>>), + Forked(Vec>) +} + /// The entry point for executing eth api RPC call - The Eth RPC interface. /// /// This type is cheap to clone and can be used concurrently @@ -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() } @@ -1103,6 +1118,34 @@ impl EthApi { .await } + pub async fn simulate_v1( + &self, + request: SimulatePayload, + block_number: Option, + ) -> Result { + 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 + // + 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. diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index 4024428a85047..ea8a9894cdf55 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -14,6 +14,7 @@ use alloy_provider::{ }; use alloy_rpc_types::{ request::TransactionRequest, + simulate::{SimulatePayload, SimulatedBlock}, trace::{ geth::{GethDebugTracingOptions, GethTrace}, parity::LocalizedTransactionTrace as Trace, @@ -197,7 +198,23 @@ impl ClientFork { Ok(res) } - /// Sends `eth_call` + /// Sends `eth_simulateV1` + pub async fn simulate_v1( + &self, + request: &SimulatePayload, + block: Option, + ) -> Result>, 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, diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index fee73eabb716e..ec0d6515bad20 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -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::{ @@ -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, @@ -36,7 +37,7 @@ 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::{ @@ -44,12 +45,13 @@ use alloy_network::{ 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, @@ -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}, }; @@ -1454,6 +1456,138 @@ impl Backend { inspector } + pub async fn simulate( + &self, + request: SimulatePayload, + block_request: Option, + ) -> Result>>, 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,