From 91eb50c59f9cae284d80517e0b72fc17e65b3051 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 18 Sep 2024 20:38:04 +0300 Subject: [PATCH] feat: `eth_simulateV1` support (#10829) Co-authored-by: Matthias Seitz --- Cargo.lock | 1 + Cargo.toml | 28 ++ book/cli/reth/node.md | 5 + crates/node/core/src/args/rpc_server.rs | 9 + crates/optimism/rpc/src/eth/call.rs | 5 + crates/optimism/rpc/src/eth/mod.rs | 1 + crates/rpc/rpc-builder/src/config.rs | 1 + crates/rpc/rpc-eth-api/src/core.rs | 12 +- crates/rpc/rpc-eth-api/src/helpers/call.rs | 212 ++++++++++-- crates/rpc/rpc-eth-types/Cargo.toml | 1 + .../rpc/rpc-eth-types/src/builder/config.rs | 11 +- crates/rpc/rpc-eth-types/src/error.rs | 5 +- crates/rpc/rpc-eth-types/src/lib.rs | 1 + crates/rpc/rpc-eth-types/src/revm_utils.rs | 15 +- crates/rpc/rpc-eth-types/src/simulate.rs | 302 ++++++++++++++++++ crates/rpc/rpc-server-types/src/constants.rs | 3 + crates/rpc/rpc/src/eth/core.rs | 18 +- crates/rpc/rpc/src/eth/helpers/call.rs | 5 + crates/rpc/rpc/src/eth/helpers/state.rs | 6 +- crates/rpc/rpc/src/eth/helpers/transaction.rs | 5 +- 20 files changed, 607 insertions(+), 39 deletions(-) create mode 100644 crates/rpc/rpc-eth-types/src/simulate.rs diff --git a/Cargo.lock b/Cargo.lock index 479607f6e..a51a10d8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8512,6 +8512,7 @@ dependencies = [ name = "reth-rpc-eth-types" version = "1.0.7" dependencies = [ + "alloy-consensus", "alloy-primitives", "alloy-sol-types", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 8c4c9c511..9b00540da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -583,3 +583,31 @@ test-fuzz = "5" tikv-jemalloc-ctl = "0.6" tikv-jemallocator = "0.6" tracy-client = "0.17.3" + +#[patch.crates-io] +#alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-serde = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} +#alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} diff --git a/book/cli/reth/node.md b/book/cli/reth/node.md index 89c43e15e..f5bdf5821 100644 --- a/book/cli/reth/node.md +++ b/book/cli/reth/node.md @@ -347,6 +347,11 @@ RPC: [default: 50000000] + --rpc.max-simulate-blocks + Maximum number of blocks for `eth_simulateV1` call + + [default: 256] + --rpc.eth-proof-window The maximum proof window for historical proof generation. This value allows for generating historical proofs up to configured number of blocks from current tip (up to `tip - window`) diff --git a/crates/node/core/src/args/rpc_server.rs b/crates/node/core/src/args/rpc_server.rs index 23af07521..15771e989 100644 --- a/crates/node/core/src/args/rpc_server.rs +++ b/crates/node/core/src/args/rpc_server.rs @@ -156,6 +156,14 @@ pub struct RpcServerArgs { )] pub rpc_gas_cap: u64, + /// Maximum number of blocks for `eth_simulateV1` call. + #[arg( + long = "rpc.max-simulate-blocks", + value_name = "BLOCKS_COUNT", + default_value_t = constants::DEFAULT_MAX_SIMULATE_BLOCKS + )] + pub rpc_max_simulate_blocks: u64, + /// The maximum proof window for historical proof generation. /// This value allows for generating historical proofs up to /// configured number of blocks from current tip (up to `tip - window`). @@ -300,6 +308,7 @@ impl Default for RpcServerArgs { rpc_max_blocks_per_filter: constants::DEFAULT_MAX_BLOCKS_PER_FILTER.into(), rpc_max_logs_per_response: (constants::DEFAULT_MAX_LOGS_PER_RESPONSE as u64).into(), rpc_gas_cap: constants::gas_oracle::RPC_DEFAULT_GAS_CAP, + rpc_max_simulate_blocks: constants::DEFAULT_MAX_SIMULATE_BLOCKS, rpc_eth_proof_window: constants::DEFAULT_ETH_PROOF_WINDOW, gas_price_oracle: GasPriceOracleArgs::default(), rpc_state_cache: RpcStateCacheArgs::default(), diff --git a/crates/optimism/rpc/src/eth/call.rs b/crates/optimism/rpc/src/eth/call.rs index 8a29f3c2f..dd9a4f2e8 100644 --- a/crates/optimism/rpc/src/eth/call.rs +++ b/crates/optimism/rpc/src/eth/call.rs @@ -33,6 +33,11 @@ where self.inner.gas_cap() } + #[inline] + fn max_simulate_blocks(&self) -> u64 { + self.inner.max_simulate_blocks() + } + #[inline] fn evm_config(&self) -> &impl ConfigureEvm
{ self.inner.evm_config() diff --git a/crates/optimism/rpc/src/eth/mod.rs b/crates/optimism/rpc/src/eth/mod.rs index c79a11a42..109a0c809 100644 --- a/crates/optimism/rpc/src/eth/mod.rs +++ b/crates/optimism/rpc/src/eth/mod.rs @@ -87,6 +87,7 @@ where ctx.cache.clone(), ctx.new_gas_price_oracle(), ctx.config.rpc_gas_cap, + ctx.config.rpc_max_simulate_blocks, ctx.config.eth_proof_window, blocking_task_pool, ctx.new_fee_history_cache(), diff --git a/crates/rpc/rpc-builder/src/config.rs b/crates/rpc/rpc-builder/src/config.rs index 1cac81f4c..4e86b2c81 100644 --- a/crates/rpc/rpc-builder/src/config.rs +++ b/crates/rpc/rpc-builder/src/config.rs @@ -95,6 +95,7 @@ impl RethRpcServerConfig for RpcServerArgs { .max_logs_per_response(self.rpc_max_logs_per_response.unwrap_or_max() as usize) .eth_proof_window(self.rpc_eth_proof_window) .rpc_gas_cap(self.rpc_gas_cap) + .rpc_max_simulate_blocks(self.rpc_max_simulate_blocks) .state_cache(self.state_cache_config()) .gpo_config(self.gas_price_oracle_config()) .proof_permits(self.rpc_proof_permits) diff --git a/crates/rpc/rpc-eth-api/src/core.rs b/crates/rpc/rpc-eth-api/src/core.rs index c2103857a..60adae2a6 100644 --- a/crates/rpc/rpc-eth-api/src/core.rs +++ b/crates/rpc/rpc-eth-api/src/core.rs @@ -9,7 +9,7 @@ use reth_primitives::{transaction::AccessListResult, BlockId, BlockNumberOrTag}; use reth_rpc_server_types::{result::internal_rpc_err, ToRpcResult}; use reth_rpc_types::{ serde_helpers::JsonStorageKey, - simulate::{SimBlock, SimulatedBlock}, + simulate::{SimulatePayload, SimulatedBlock}, state::{EvmOverrides, StateOverride}, AnyTransactionReceipt, BlockOverrides, Bundle, EIP1186AccountProofResponse, EthCallResponse, FeeHistory, Header, Index, StateContext, SyncStatus, TransactionRequest, Work, @@ -211,9 +211,9 @@ pub trait EthApi { #[method(name = "simulateV1")] async fn simulate_v1( &self, - opts: SimBlock, + opts: SimulatePayload, block_number: Option, - ) -> RpcResult>; + ) -> RpcResult>>; /// Executes a new message call immediately without creating a transaction on the block chain. #[method(name = "call")] @@ -618,11 +618,11 @@ where /// Handler for: `eth_simulateV1` async fn simulate_v1( &self, - opts: SimBlock, + payload: SimulatePayload, block_number: Option, - ) -> RpcResult> { + ) -> RpcResult>>> { trace!(target: "rpc::eth", ?block_number, "Serving eth_simulateV1"); - Ok(EthCall::simulate_v1(self, opts, block_number).await?) + Ok(EthCall::simulate_v1(self, payload, block_number).await?) } /// Handler for: `eth_call` diff --git a/crates/rpc/rpc-eth-api/src/helpers/call.rs b/crates/rpc/rpc-eth-api/src/helpers/call.rs index 06e146955..3bfcd5bd1 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/call.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/call.rs @@ -7,6 +7,7 @@ use futures::Future; use reth_chainspec::MIN_TRANSACTION_GAS; use reth_evm::{ConfigureEvm, ConfigureEvmEnv}; use reth_primitives::{ + basefee::calc_next_block_base_fee, revm_primitives::{ BlockEnv, CfgEnvWithHandlerCfg, EnvWithHandlerCfg, ExecutionResult, HaltReason, ResultAndState, TransactTo, TxEnv, @@ -14,7 +15,7 @@ use reth_primitives::{ transaction::AccessListResult, Header, TransactionSignedEcRecovered, }; -use reth_provider::{ChainSpecProvider, StateProvider}; +use reth_provider::{ChainSpecProvider, HeaderProvider, StateProvider}; use reth_revm::{database::StateProviderDatabase, db::CacheDB, DatabaseRef}; use reth_rpc_eth_types::{ cache::db::{StateCacheDbRefMutWrapper, StateProviderTraitObjWrapper}, @@ -23,16 +24,18 @@ use reth_rpc_eth_types::{ apply_block_overrides, apply_state_overrides, caller_gas_allowance, cap_tx_gas_limit_with_caller_allowance, get_precompiles, CallFees, }, + simulate::{self, EthSimulateError}, EthApiError, RevertError, RpcInvalidTransactionError, StateCacheDb, }; use reth_rpc_server_types::constants::gas_oracle::{CALL_STIPEND_GAS, ESTIMATE_GAS_ERROR_RATIO}; use reth_rpc_types::{ - simulate::{SimBlock, SimulatedBlock}, + simulate::{SimBlock, SimulatePayload, SimulatedBlock}, state::{EvmOverrides, StateOverride}, - BlockId, Bundle, EthCallResponse, StateContext, TransactionInfo, TransactionRequest, + Block, BlockId, Bundle, EthCallResponse, StateContext, TransactionInfo, TransactionRequest, + WithOtherFields, }; -use revm::{Database, DatabaseCommit}; -use revm_inspectors::access_list::AccessListInspector; +use revm::{Database, DatabaseCommit, GetInspector}; +use revm_inspectors::{access_list::AccessListInspector, transfer::TransferInspector}; use tracing::trace; use super::{LoadBlock, LoadPendingBlock, LoadState, LoadTransaction, SpawnBlocking, Trace}; @@ -56,10 +59,158 @@ pub trait EthCall: Call + LoadPendingBlock { /// See also: fn simulate_v1( &self, - _opts: SimBlock, - _block_number: Option, - ) -> impl Future, Self::Error>> + Send { - async move { Err(EthApiError::Unsupported("eth_simulateV1 is not supported.").into()) } + payload: SimulatePayload, + block: Option, + ) -> impl Future< + Output = Result< + Vec>>>, + Self::Error, + >, + > + Send + where + Self: LoadBlock, + { + async move { + if payload.block_state_calls.len() > self.max_simulate_blocks() as usize { + return Err(EthApiError::InvalidParams("too many blocks.".to_string()).into()) + } + + let SimulatePayload { + block_state_calls, + trace_transfers, + validation, + return_full_transactions, + } = payload; + + if block_state_calls.is_empty() { + return Err(EthApiError::InvalidParams(String::from("calls are empty.")).into()) + } + + // Build cfg and block env, we'll reuse those. + let (mut cfg, mut block_env, block) = + self.evm_env_at(block.unwrap_or_default()).await?; + + // Gas cap for entire operation + let total_gas_limit = self.call_gas_limit() as u128; + + let base_block = self.block(block).await?.ok_or(EthApiError::HeaderNotFound(block))?; + let mut parent_hash = base_block.header.hash(); + let total_difficulty = LoadPendingBlock::provider(self) + .header_td_by_number(block_env.number.to()) + .map_err(Self::Error::from_eth_err)? + .ok_or(EthApiError::HeaderNotFound(block))?; + + // Only enforce base fee if validation is enabled + cfg.disable_base_fee = !validation; + // Always disable EIP-3607 + cfg.disable_eip3607 = true; + + let this = self.clone(); + self.spawn_with_state_at_block(block, move |state| { + let mut db = CacheDB::new(StateProviderDatabase::new(state)); + let mut blocks: Vec< + SimulatedBlock>>, + > = Vec::with_capacity(block_state_calls.len()); + let mut gas_used = 0; + for block in block_state_calls { + // Increase number and timestamp for every new block + block_env.number += U256::from(1); + block_env.timestamp += U256::from(1); + + if validation { + let chain_spec = LoadPendingBlock::provider(&this).chain_spec(); + let base_fee_params = + chain_spec.base_fee_params_at_timestamp(block_env.timestamp.to()); + let base_fee = if let Some(latest) = blocks.last() { + let header = &latest.inner.header; + calc_next_block_base_fee( + header.gas_used, + header.gas_limit, + header.base_fee_per_gas.unwrap_or_default(), + base_fee_params, + ) + } else { + base_block + .header + .next_block_base_fee(base_fee_params) + .unwrap_or_default() as u128 + }; + block_env.basefee = U256::from(base_fee); + } else { + block_env.basefee = U256::ZERO; + } + + let SimBlock { block_overrides, state_overrides, mut calls } = block; + + if let Some(block_overrides) = block_overrides { + apply_block_overrides(block_overrides, &mut db, &mut block_env); + } + if let Some(state_overrides) = state_overrides { + apply_state_overrides(state_overrides, &mut db)?; + } + + if (total_gas_limit - gas_used) < block_env.gas_limit.to() { + return Err( + EthApiError::Other(Box::new(EthSimulateError::GasLimitReached)).into() + ) + } + + // Resolve transactions, populate missing fields and enforce calls correctness. + let transactions = simulate::resolve_transactions( + &mut calls, + validation, + block_env.gas_limit.to(), + cfg.chain_id, + &mut db, + )?; + + let mut calls = calls.into_iter().peekable(); + let mut results = Vec::with_capacity(calls.len()); + + while let Some(tx) = calls.next() { + let env = this.build_call_evm_env(cfg.clone(), block_env.clone(), tx)?; + + let (res, env) = { + if trace_transfers { + this.transact_with_inspector( + &mut db, + env, + TransferInspector::new(false).with_logs(true), + )? + } else { + this.transact(&mut db, env)? + } + }; + + if calls.peek().is_some() { + // need to apply the state changes of this call before executing the + // next call + db.commit(res.state); + } + + results.push((env.tx.caller, res.result)); + } + + let block = simulate::build_block( + results, + transactions, + &block_env, + parent_hash, + total_difficulty, + return_full_transactions, + &db, + )?; + + parent_hash = block.inner.header.hash; + gas_used += block.inner.header.gas_used; + + blocks.push(block); + } + + Ok(blocks) + }) + .await + } } /// Executes the call request (`eth_call`) and returns the output @@ -303,6 +454,9 @@ pub trait Call: LoadState + SpawnBlocking { /// Data access in default trait method implementations. fn call_gas_limit(&self) -> u64; + /// Returns the maximum number of blocks accepted for `eth_simulateV1`. + fn max_simulate_blocks(&self) -> u64; + /// Returns a handle for reading evm config. /// /// Data access in default (L1) trait method implementations. @@ -334,6 +488,24 @@ pub trait Call: LoadState + SpawnBlocking { Ok((res, env)) } + /// Executes the [`EnvWithHandlerCfg`] against the given [Database] without committing state + /// changes. + fn transact_with_inspector( + &self, + db: DB, + env: EnvWithHandlerCfg, + inspector: impl GetInspector, + ) -> Result<(ResultAndState, EnvWithHandlerCfg), Self::Error> + where + DB: Database, + EthApiError: From, + { + let mut evm = self.evm_config().evm_with_env_and_inspector(db, env, inspector); + let res = evm.transact().map_err(Self::Error::from_evm_err)?; + let (_, env) = evm.into_db_and_env_with_handler_cfg(); + Ok((res, env)) + } + /// Executes the call request at the given [`BlockId`]. fn transact_call_at( &self, @@ -854,7 +1026,8 @@ pub trait Call: LoadState + SpawnBlocking { blob_versioned_hashes, max_fee_per_blob_gas, authorization_list, - .. + transaction_type: _, + sidecar: _, } = request; let CallFees { max_priority_fee_per_gas, gas_price, max_fee_per_blob_gas } = @@ -956,25 +1129,16 @@ pub trait Call: LoadState + SpawnBlocking { // set nonce to None so that the correct nonce is chosen by the EVM request.nonce = None; - // apply block overrides, we need to apply them first so that they take effect when we we - // create the evm env via `build_call_evm_env`, e.g. basefee - if let Some(mut block_overrides) = overrides.block { - if let Some(block_hashes) = block_overrides.block_hash.take() { - // override block hashes - db.block_hashes - .extend(block_hashes.into_iter().map(|(num, hash)| (U256::from(num), hash))) - } - apply_block_overrides(*block_overrides, &mut block); + if let Some(block_overrides) = overrides.block { + apply_block_overrides(*block_overrides, db, &mut block); + } + if let Some(state_overrides) = overrides.state { + apply_state_overrides(state_overrides, db)?; } let request_gas = request.gas; let mut env = self.build_call_evm_env(cfg, block, request)?; - // apply state overrides - if let Some(state_overrides) = overrides.state { - apply_state_overrides(state_overrides, db)?; - } - if request_gas.is_none() { // No gas limit was provided in the request, so we need to cap the transaction gas limit if env.tx.gas_price > U256::ZERO { diff --git a/crates/rpc/rpc-eth-types/Cargo.toml b/crates/rpc/rpc-eth-types/Cargo.toml index 839eb219e..18d8767f0 100644 --- a/crates/rpc/rpc-eth-types/Cargo.toml +++ b/crates/rpc/rpc-eth-types/Cargo.toml @@ -30,6 +30,7 @@ reth-trie.workspace = true # ethereum alloy-primitives.workspace = true +alloy-consensus.workspace = true alloy-sol-types.workspace = true revm.workspace = true revm-inspectors.workspace = true diff --git a/crates/rpc/rpc-eth-types/src/builder/config.rs b/crates/rpc/rpc-eth-types/src/builder/config.rs index 2edc81e8d..a016d0215 100644 --- a/crates/rpc/rpc-eth-types/src/builder/config.rs +++ b/crates/rpc/rpc-eth-types/src/builder/config.rs @@ -7,7 +7,7 @@ use crate::{ }; use reth_rpc_server_types::constants::{ default_max_tracing_requests, DEFAULT_ETH_PROOF_WINDOW, DEFAULT_MAX_BLOCKS_PER_FILTER, - DEFAULT_MAX_LOGS_PER_RESPONSE, DEFAULT_PROOF_PERMITS, + DEFAULT_MAX_LOGS_PER_RESPONSE, DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_PROOF_PERMITS, }; use serde::{Deserialize, Serialize}; @@ -33,6 +33,8 @@ pub struct EthConfig { /// /// Defaults to [`RPC_DEFAULT_GAS_CAP`] pub rpc_gas_cap: u64, + /// Max number of blocks for `eth_simulateV1`. + pub rpc_max_simulate_blocks: u64, /// /// Sets TTL for stale filters pub stale_filter_ttl: Duration, @@ -62,6 +64,7 @@ impl Default for EthConfig { max_blocks_per_filter: DEFAULT_MAX_BLOCKS_PER_FILTER, max_logs_per_response: DEFAULT_MAX_LOGS_PER_RESPONSE, rpc_gas_cap: RPC_DEFAULT_GAS_CAP.into(), + rpc_max_simulate_blocks: DEFAULT_MAX_SIMULATE_BLOCKS, stale_filter_ttl: DEFAULT_STALE_FILTER_TTL, fee_history_cache: FeeHistoryCacheConfig::default(), proof_permits: DEFAULT_PROOF_PERMITS, @@ -106,6 +109,12 @@ impl EthConfig { self } + /// Configures the maximum gas limit for `eth_call` and call tracing RPC methods + pub const fn rpc_max_simulate_blocks(mut self, max_blocks: u64) -> Self { + self.rpc_max_simulate_blocks = max_blocks; + self + } + /// Configures the maximum proof window for historical proof generation. pub const fn eth_proof_window(mut self, window: u64) -> Self { self.eth_proof_window = window; diff --git a/crates/rpc/rpc-eth-types/src/error.rs b/crates/rpc/rpc-eth-types/src/error.rs index 1ead29d2c..977300cad 100644 --- a/crates/rpc/rpc-eth-types/src/error.rs +++ b/crates/rpc/rpc-eth-types/src/error.rs @@ -422,7 +422,7 @@ impl RpcInvalidTransactionError { impl RpcInvalidTransactionError { /// Returns the rpc error code for this error. - const fn error_code(&self) -> i32 { + pub const fn error_code(&self) -> i32 { match self { Self::InvalidChainId | Self::GasTooLow | Self::GasTooHigh => { EthRpcErrorCode::InvalidInput.code() @@ -576,7 +576,8 @@ impl RevertError { } } - const fn error_code(&self) -> i32 { + /// Returns error code to return for this error. + pub const fn error_code(&self) -> i32 { EthRpcErrorCode::ExecutionError.code() } } diff --git a/crates/rpc/rpc-eth-types/src/lib.rs b/crates/rpc/rpc-eth-types/src/lib.rs index 1897a9fd7..fba893c15 100644 --- a/crates/rpc/rpc-eth-types/src/lib.rs +++ b/crates/rpc/rpc-eth-types/src/lib.rs @@ -18,6 +18,7 @@ pub mod logs_utils; pub mod pending_block; pub mod receipt; pub mod revm_utils; +pub mod simulate; pub mod transaction; pub mod utils; diff --git a/crates/rpc/rpc-eth-types/src/revm_utils.rs b/crates/rpc/rpc-eth-types/src/revm_utils.rs index dd22c0942..0e1cb9d7d 100644 --- a/crates/rpc/rpc-eth-types/src/revm_utils.rs +++ b/crates/rpc/rpc-eth-types/src/revm_utils.rs @@ -206,8 +206,12 @@ impl CallFees { } } -/// Applies the given block overrides to the env -pub fn apply_block_overrides(overrides: BlockOverrides, env: &mut BlockEnv) { +/// Applies the given block overrides to the env and updates overridden block hashes in the db. +pub fn apply_block_overrides( + overrides: BlockOverrides, + db: &mut CacheDB, + env: &mut BlockEnv, +) { let BlockOverrides { number, difficulty, @@ -216,9 +220,14 @@ pub fn apply_block_overrides(overrides: BlockOverrides, env: &mut BlockEnv) { coinbase, random, base_fee, - block_hash: _, + block_hash, } = overrides; + if let Some(block_hashes) = block_hash { + // override block hashes + db.block_hashes.extend(block_hashes.into_iter().map(|(num, hash)| (U256::from(num), hash))) + } + if let Some(number) = number { env.number = number; } diff --git a/crates/rpc/rpc-eth-types/src/simulate.rs b/crates/rpc/rpc-eth-types/src/simulate.rs new file mode 100644 index 000000000..c36f77599 --- /dev/null +++ b/crates/rpc/rpc-eth-types/src/simulate.rs @@ -0,0 +1,302 @@ +//! Utilities for serving `eth_simulateV1` + +use alloy_consensus::{TxEip4844Variant, TxType, TypedTransaction}; +use jsonrpsee_types::ErrorObject; +use reth_primitives::{ + logs_bloom, + proofs::{calculate_receipt_root, calculate_transaction_root}, + BlockWithSenders, Receipt, Signature, Transaction, TransactionSigned, TransactionSignedNoHash, +}; +use reth_revm::database::StateProviderDatabase; +use reth_rpc_server_types::result::rpc_err; +use reth_rpc_types::{ + simulate::{SimCallResult, SimulateError, SimulatedBlock}, + Block, BlockTransactionsKind, ToRpcError, TransactionRequest, WithOtherFields, +}; +use reth_rpc_types_compat::block::from_block; +use reth_storage_api::StateRootProvider; +use reth_trie::{HashedPostState, HashedStorage}; +use revm::{db::CacheDB, Database}; +use revm_primitives::{keccak256, Address, BlockEnv, Bytes, ExecutionResult, TxKind, B256, U256}; + +use crate::{ + cache::db::StateProviderTraitObjWrapper, EthApiError, RevertError, RpcInvalidTransactionError, +}; + +/// Errors which may occur during `eth_simulateV1` execution. +#[derive(Debug, thiserror::Error)] +pub enum EthSimulateError { + /// Total gas limit of transactions for the block exceeds the block gas limit. + #[error("Block gas limit exceeded by the block's transactions")] + BlockGasLimitExceeded, + /// Max gas limit for entire operation exceeded. + #[error("Client adjustable limit reached")] + GasLimitReached, +} + +impl EthSimulateError { + const fn error_code(&self) -> i32 { + match self { + Self::BlockGasLimitExceeded => -38015, + Self::GasLimitReached => -38026, + } + } +} + +impl ToRpcError for EthSimulateError { + fn to_rpc_error(&self) -> ErrorObject<'static> { + rpc_err(self.error_code(), self.to_string(), None) + } +} + +/// Goes over the list of [`TransactionRequest`]s and populates missing fields trying to resolve +/// them into [`TransactionSigned`]. +/// +/// If validation is enabled, the function will return error if any of the transactions can't be +/// built right away. +pub fn resolve_transactions( + txs: &mut [TransactionRequest], + validation: bool, + block_gas_limit: u128, + chain_id: u64, + db: &mut DB, +) -> Result, EthApiError> +where + EthApiError: From, +{ + let mut transactions = Vec::with_capacity(txs.len()); + + let default_gas_limit = { + let total_specified_gas = txs.iter().filter_map(|tx| tx.gas).sum::(); + let txs_without_gas_limit = txs.iter().filter(|tx| tx.gas.is_none()).count(); + + if total_specified_gas > block_gas_limit { + return Err(EthApiError::Other(Box::new(EthSimulateError::BlockGasLimitExceeded))) + } + + if txs_without_gas_limit > 0 { + (block_gas_limit - total_specified_gas) / txs_without_gas_limit as u128 + } else { + 0 + } + }; + + for tx in txs { + if tx.buildable_type().is_none() && validation { + return Err(EthApiError::TransactionConversionError); + } + // If we're missing any fields and validation is disabled, we try filling nonce, gas and + // gas price. + let tx_type = tx.preferred_type(); + + let from = if let Some(from) = tx.from { + from + } else { + tx.from = Some(Address::ZERO); + Address::ZERO + }; + + if tx.nonce.is_none() { + tx.nonce = Some(db.basic(from)?.map(|acc| acc.nonce).unwrap_or_default()); + } + + if tx.gas.is_none() { + tx.gas = Some(default_gas_limit); + } + + if tx.chain_id.is_none() { + tx.chain_id = Some(chain_id); + } + + if tx.to.is_none() { + tx.to = Some(TxKind::Create); + } + + match tx_type { + TxType::Legacy | TxType::Eip2930 => { + if tx.gas_price.is_none() { + tx.gas_price = Some(0); + } + } + _ => { + if tx.max_fee_per_gas.is_none() { + tx.max_fee_per_gas = Some(0); + tx.max_priority_fee_per_gas = Some(0); + } + } + } + + let Ok(tx) = tx.clone().build_typed_tx() else { + return Err(EthApiError::TransactionConversionError) + }; + + // Create an empty signature for the transaction. + let signature = + Signature { odd_y_parity: false, r: Default::default(), s: Default::default() }; + + let tx = match tx { + TypedTransaction::Legacy(tx) => { + TransactionSignedNoHash { transaction: Transaction::Legacy(tx), signature } + .with_hash() + } + TypedTransaction::Eip2930(tx) => { + TransactionSignedNoHash { transaction: Transaction::Eip2930(tx), signature } + .with_hash() + } + TypedTransaction::Eip1559(tx) => { + TransactionSignedNoHash { transaction: Transaction::Eip1559(tx), signature } + .with_hash() + } + TypedTransaction::Eip4844(tx) => { + let tx = match tx { + TxEip4844Variant::TxEip4844(tx) => tx, + TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx, + }; + TransactionSignedNoHash { transaction: Transaction::Eip4844(tx), signature } + .with_hash() + } + TypedTransaction::Eip7702(tx) => { + TransactionSignedNoHash { transaction: Transaction::Eip7702(tx), signature } + .with_hash() + } + }; + + transactions.push(tx); + } + + Ok(transactions) +} + +/// Handles outputs of the calls execution and builds a [`SimulatedBlock`]. +pub fn build_block( + results: Vec<(Address, ExecutionResult)>, + transactions: Vec, + block_env: &BlockEnv, + parent_hash: B256, + total_difficulty: U256, + full_transactions: bool, + db: &CacheDB>>, +) -> Result>>, EthApiError> { + let mut calls: Vec = Vec::with_capacity(results.len()); + let mut senders = Vec::with_capacity(results.len()); + let mut receipts = Vec::new(); + + let mut log_index = 0; + for (transaction_index, ((sender, result), tx)) in + results.into_iter().zip(transactions.iter()).enumerate() + { + senders.push(sender); + + let call = match result { + ExecutionResult::Halt { reason, gas_used } => { + let error = RpcInvalidTransactionError::halt(reason, tx.gas_limit()); + SimCallResult { + return_value: Bytes::new(), + error: Some(SimulateError { + code: error.error_code(), + message: error.to_string(), + }), + gas_used, + logs: Vec::new(), + status: false, + } + } + ExecutionResult::Revert { output, gas_used } => { + let error = RevertError::new(output.clone()); + SimCallResult { + return_value: output, + error: Some(SimulateError { + code: error.error_code(), + message: error.to_string(), + }), + gas_used, + status: false, + logs: Vec::new(), + } + } + ExecutionResult::Success { output, gas_used, logs, .. } => SimCallResult { + return_value: output.into_data(), + error: None, + gas_used, + logs: logs + .into_iter() + .map(|log| { + log_index += 1; + reth_rpc_types::Log { + inner: log, + log_index: Some(log_index - 1), + transaction_index: Some(transaction_index as u64), + transaction_hash: Some(tx.hash()), + block_number: Some(block_env.number.to()), + block_timestamp: Some(block_env.timestamp.to()), + ..Default::default() + } + }) + .collect(), + status: true, + }, + }; + + receipts.push( + #[allow(clippy::needless_update)] + Receipt { + tx_type: tx.tx_type(), + success: call.status, + cumulative_gas_used: call.gas_used + calls.iter().map(|c| c.gas_used).sum::(), + logs: call.logs.iter().map(|log| &log.inner).cloned().collect(), + ..Default::default() + } + .into(), + ); + + calls.push(call); + } + + let mut hashed_state = HashedPostState::default(); + for (address, account) in &db.accounts { + let hashed_address = keccak256(address); + hashed_state.accounts.insert(hashed_address, Some(account.info.clone().into())); + + let storage = hashed_state + .storages + .entry(hashed_address) + .or_insert_with(|| HashedStorage::new(account.account_state.is_storage_cleared())); + + for (slot, value) in &account.storage { + let slot = B256::from(*slot); + let hashed_slot = keccak256(slot); + storage.storage.insert(hashed_slot, *value); + } + } + + let state_root = db.db.0.state_root(hashed_state)?; + + let header = reth_primitives::Header { + beneficiary: block_env.coinbase, + difficulty: block_env.difficulty, + number: block_env.number.to(), + timestamp: block_env.timestamp.to(), + base_fee_per_gas: Some(block_env.basefee.to()), + gas_limit: block_env.gas_limit.to(), + gas_used: calls.iter().map(|c| c.gas_used).sum::(), + blob_gas_used: Some(0), + parent_hash, + receipts_root: calculate_receipt_root(&receipts), + transactions_root: calculate_transaction_root(&transactions), + state_root, + logs_bloom: logs_bloom(receipts.iter().flat_map(|r| r.receipt.logs.iter())), + mix_hash: block_env.prevrandao.unwrap_or_default(), + ..Default::default() + }; + + let block = BlockWithSenders { + block: reth_primitives::Block { header, body: transactions, ..Default::default() }, + senders, + }; + + let txs_kind = + if full_transactions { BlockTransactionsKind::Full } else { BlockTransactionsKind::Hashes }; + + let block = from_block(block, total_difficulty, txs_kind, None)?; + Ok(SimulatedBlock { inner: block, calls }) +} diff --git a/crates/rpc/rpc-server-types/src/constants.rs b/crates/rpc/rpc-server-types/src/constants.rs index d6742584a..0bc441819 100644 --- a/crates/rpc/rpc-server-types/src/constants.rs +++ b/crates/rpc/rpc-server-types/src/constants.rs @@ -45,6 +45,9 @@ pub const DEFAULT_ENGINE_API_IPC_ENDPOINT: &str = r"\\.\pipe\reth_engine_api.ipc #[cfg(not(windows))] pub const DEFAULT_ENGINE_API_IPC_ENDPOINT: &str = "/tmp/reth_engine_api.ipc"; +/// The default limit for blocks count in `eth_simulateV1`. +pub const DEFAULT_MAX_SIMULATE_BLOCKS: u64 = 256; + /// The default eth historical proof window. pub const DEFAULT_ETH_PROOF_WINDOW: u64 = 0; diff --git a/crates/rpc/rpc/src/eth/core.rs b/crates/rpc/rpc/src/eth/core.rs index b1848d9a4..73b10b05e 100644 --- a/crates/rpc/rpc/src/eth/core.rs +++ b/crates/rpc/rpc/src/eth/core.rs @@ -57,6 +57,7 @@ where eth_cache: EthStateCache, gas_oracle: GasPriceOracle, gas_cap: impl Into, + max_simulate_blocks: u64, eth_proof_window: u64, blocking_task_pool: BlockingTaskPool, fee_history_cache: FeeHistoryCache, @@ -70,6 +71,7 @@ where eth_cache, gas_oracle, gas_cap, + max_simulate_blocks, eth_proof_window, blocking_task_pool, fee_history_cache, @@ -107,6 +109,7 @@ where ctx.cache.clone(), ctx.new_gas_price_oracle(), ctx.config.rpc_gas_cap, + ctx.config.rpc_max_simulate_blocks, ctx.config.eth_proof_window, blocking_task_pool, ctx.new_fee_history_cache(), @@ -186,6 +189,8 @@ pub struct EthApiInner { gas_oracle: GasPriceOracle, /// Maximum gas limit for `eth_call` and call tracing RPC methods. gas_cap: u64, + /// Maximum number of blocks for `eth_simulateV1`. + max_simulate_blocks: u64, /// The maximum number of blocks into the past for generating state proofs. eth_proof_window: u64, /// The block number at which the node started @@ -218,6 +223,7 @@ where eth_cache: EthStateCache, gas_oracle: GasPriceOracle, gas_cap: impl Into, + max_simulate_blocks: u64, eth_proof_window: u64, blocking_task_pool: BlockingTaskPool, fee_history_cache: FeeHistoryCache, @@ -244,6 +250,7 @@ where eth_cache, gas_oracle, gas_cap: gas_cap.into().into(), + max_simulate_blocks, eth_proof_window, starting_block, task_spawner: Box::new(task_spawner), @@ -305,6 +312,12 @@ impl EthApiInner u64 { + self.max_simulate_blocks + } + /// Returns a handle to the gas oracle. #[inline] pub const fn gas_oracle(&self) -> &GasPriceOracle { @@ -364,7 +377,9 @@ mod tests { use reth_rpc_eth_types::{ EthStateCache, FeeHistoryCache, FeeHistoryCacheConfig, GasPriceOracle, }; - use reth_rpc_server_types::constants::{DEFAULT_ETH_PROOF_WINDOW, DEFAULT_PROOF_PERMITS}; + use reth_rpc_server_types::constants::{ + DEFAULT_ETH_PROOF_WINDOW, DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_PROOF_PERMITS, + }; use reth_rpc_types::FeeHistory; use reth_tasks::pool::BlockingTaskPool; use reth_testing_utils::{generators, generators::Rng}; @@ -397,6 +412,7 @@ mod tests { cache.clone(), GasPriceOracle::new(provider, Default::default(), cache), gas_cap, + DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_ETH_PROOF_WINDOW, BlockingTaskPool::build().expect("failed to build tracing pool"), fee_history_cache, diff --git a/crates/rpc/rpc/src/eth/helpers/call.rs b/crates/rpc/rpc/src/eth/helpers/call.rs index 3e93a5a7a..396bf9bd0 100644 --- a/crates/rpc/rpc/src/eth/helpers/call.rs +++ b/crates/rpc/rpc/src/eth/helpers/call.rs @@ -21,6 +21,11 @@ where self.inner.gas_cap() } + #[inline] + fn max_simulate_blocks(&self) -> u64 { + self.inner.max_simulate_blocks() + } + #[inline] fn evm_config(&self) -> &impl ConfigureEvm
{ self.inner.evm_config() diff --git a/crates/rpc/rpc/src/eth/helpers/state.rs b/crates/rpc/rpc/src/eth/helpers/state.rs index 2d2ce5d5a..c0696adc5 100644 --- a/crates/rpc/rpc/src/eth/helpers/state.rs +++ b/crates/rpc/rpc/src/eth/helpers/state.rs @@ -52,7 +52,9 @@ mod tests { use reth_rpc_eth_types::{ EthStateCache, FeeHistoryCache, FeeHistoryCacheConfig, GasPriceOracle, }; - use reth_rpc_server_types::constants::{DEFAULT_ETH_PROOF_WINDOW, DEFAULT_PROOF_PERMITS}; + use reth_rpc_server_types::constants::{ + DEFAULT_ETH_PROOF_WINDOW, DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_PROOF_PERMITS, + }; use reth_tasks::pool::BlockingTaskPool; use reth_transaction_pool::test_utils::{testing_pool, TestPool}; use std::collections::HashMap; @@ -70,6 +72,7 @@ mod tests { cache.clone(), GasPriceOracle::new(NoopProvider::default(), Default::default(), cache.clone()), ETHEREUM_BLOCK_GAS_LIMIT, + DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_ETH_PROOF_WINDOW, BlockingTaskPool::build().expect("failed to build tracing pool"), FeeHistoryCache::new(cache, FeeHistoryCacheConfig::default()), @@ -96,6 +99,7 @@ mod tests { cache.clone(), GasPriceOracle::new(mock_provider, Default::default(), cache.clone()), ETHEREUM_BLOCK_GAS_LIMIT, + DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_ETH_PROOF_WINDOW, BlockingTaskPool::build().expect("failed to build tracing pool"), FeeHistoryCache::new(cache, FeeHistoryCacheConfig::default()), diff --git a/crates/rpc/rpc/src/eth/helpers/transaction.rs b/crates/rpc/rpc/src/eth/helpers/transaction.rs index 7188ecf0a..17eff7b35 100644 --- a/crates/rpc/rpc/src/eth/helpers/transaction.rs +++ b/crates/rpc/rpc/src/eth/helpers/transaction.rs @@ -62,7 +62,9 @@ mod tests { use reth_rpc_eth_types::{ EthStateCache, FeeHistoryCache, FeeHistoryCacheConfig, GasPriceOracle, }; - use reth_rpc_server_types::constants::{DEFAULT_ETH_PROOF_WINDOW, DEFAULT_PROOF_PERMITS}; + use reth_rpc_server_types::constants::{ + DEFAULT_ETH_PROOF_WINDOW, DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_PROOF_PERMITS, + }; use reth_tasks::pool::BlockingTaskPool; use reth_transaction_pool::{test_utils::testing_pool, TransactionPool}; @@ -86,6 +88,7 @@ mod tests { cache.clone(), GasPriceOracle::new(noop_provider, Default::default(), cache.clone()), ETHEREUM_BLOCK_GAS_LIMIT, + DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_ETH_PROOF_WINDOW, BlockingTaskPool::build().expect("failed to build tracing pool"), fee_history_cache,