Skip to main content

reth_rpc_eth_types/
simulate.rs

1//! Utilities for serving `eth_simulateV1`
2
3use crate::{
4    error::{api::FromEthApiError, FromEvmError, ToRpcError},
5    EthApiError,
6};
7use alloy_consensus::{transaction::TxHashRef, BlockHeader, Transaction as _};
8use alloy_eips::eip2718::WithEncoded;
9use alloy_evm::precompiles::PrecompilesMap;
10use alloy_network::TransactionBuilder;
11use alloy_rpc_types_eth::{
12    simulate::{SimCallResult, SimulateError, SimulatedBlock},
13    state::StateOverride,
14    BlockTransactionsKind,
15};
16use jsonrpsee_types::ErrorObject;
17use reth_evm::{
18    execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
19    Evm, HaltReasonFor,
20};
21use reth_primitives_traits::{BlockBody as _, BlockTy, NodePrimitives, Recovered, RecoveredBlock};
22use reth_rpc_convert::{RpcBlock, RpcConvert, RpcTxReq};
23use reth_rpc_server_types::result::rpc_err;
24use reth_storage_api::noop::NoopProvider;
25use revm::{
26    context::Block,
27    context_interface::result::ExecutionResult,
28    primitives::{Address, Bytes, TxKind, U256},
29    Database,
30};
31
32/// Error code for execution reverted in `eth_simulateV1`.
33///
34/// Consistent with `eth_call` revert error code.
35///
36/// <https://github.com/ethereum/execution-apis/pull/748>
37pub const SIMULATE_REVERT_CODE: i32 = 3;
38
39/// Error code for VM execution errors (e.g., out of gas) in `eth_simulateV1`.
40///
41/// <https://github.com/ethereum/execution-apis>
42pub const SIMULATE_VM_ERROR_CODE: i32 = -32015;
43
44/// Errors which may occur during `eth_simulateV1` execution.
45#[derive(Debug, thiserror::Error)]
46pub enum EthSimulateError {
47    /// Total gas limit of transactions for the block exceeds the block gas limit.
48    #[error("Block gas limit exceeded by the block's transactions")]
49    BlockGasLimitExceeded,
50    /// Max gas limit for entire operation exceeded.
51    #[error("Client adjustable limit reached")]
52    GasLimitReached,
53    /// Block number in sequence did not increase.
54    #[error("block numbers must be in order: {got} <= {parent}")]
55    BlockNumberInvalid {
56        /// The block number that was provided.
57        got: u64,
58        /// The parent block number.
59        parent: u64,
60    },
61    /// Block timestamp in sequence did not increase.
62    #[error("block timestamps must be in order: {got} <= {parent}")]
63    BlockTimestampInvalid {
64        /// The block timestamp that was provided.
65        got: u64,
66        /// The parent block timestamp.
67        parent: u64,
68    },
69    /// Transaction nonce is too low.
70    #[error("nonce too low: next nonce {state}, tx nonce {tx}")]
71    NonceTooLow {
72        /// Transaction nonce.
73        tx: u64,
74        /// Current state nonce.
75        state: u64,
76    },
77    /// Transaction nonce is too high.
78    #[error("nonce too high")]
79    NonceTooHigh,
80    /// Transaction's baseFeePerGas is too low.
81    #[error("max fee per gas less than block base fee")]
82    BaseFeePerGasTooLow,
83    /// Not enough gas provided to pay for intrinsic gas.
84    #[error("intrinsic gas too low")]
85    IntrinsicGasTooLow,
86    /// Insufficient funds to pay for gas fees and value.
87    #[error("insufficient funds for gas * price + value: have {balance} want {cost}")]
88    InsufficientFunds {
89        /// Transaction cost.
90        cost: U256,
91        /// Sender balance.
92        balance: U256,
93    },
94    /// Sender is not an EOA.
95    #[error("sender is not an EOA")]
96    SenderNotEOA,
97    /// Max init code size exceeded.
98    #[error("max initcode size exceeded")]
99    MaxInitCodeSizeExceeded,
100    /// Attempted to move a non-precompile address.
101    #[error("account {0} is not a precompile")]
102    NotAPrecompile(Address),
103}
104
105impl EthSimulateError {
106    /// Returns the JSON-RPC error code for a `eth_simulateV1` error.
107    pub const fn error_code(&self) -> i32 {
108        match self {
109            Self::NonceTooLow { .. } => -38010,
110            Self::NonceTooHigh => -38011,
111            Self::BaseFeePerGasTooLow => -38012,
112            Self::IntrinsicGasTooLow => -38013,
113            Self::InsufficientFunds { .. } => -38014,
114            Self::BlockGasLimitExceeded => -38015,
115            Self::BlockNumberInvalid { .. } => -38020,
116            Self::BlockTimestampInvalid { .. } => -38021,
117            Self::SenderNotEOA => -38024,
118            Self::MaxInitCodeSizeExceeded => -38025,
119            Self::GasLimitReached => -38026,
120            Self::NotAPrecompile(_) => -32000,
121        }
122    }
123}
124
125impl ToRpcError for EthSimulateError {
126    fn to_rpc_error(&self) -> ErrorObject<'static> {
127        rpc_err(self.error_code(), self.to_string(), None)
128    }
129}
130
131/// Applies precompile move overrides from state overrides to the EVM's precompiles map.
132///
133/// This function processes `movePrecompileToAddress` entries from the state overrides and
134/// moves precompiles from their original addresses to new addresses. The original address
135/// is cleared (precompile removed) and the precompile is installed at the destination address.
136pub fn apply_precompile_overrides(
137    state_overrides: &StateOverride,
138    precompiles: &mut PrecompilesMap,
139) -> Result<(), EthSimulateError> {
140    let moves: Vec<_> = state_overrides
141        .iter()
142        .filter_map(|(source, account_override)| {
143            account_override.move_precompile_to.map(|dest| (*source, dest))
144        })
145        .collect();
146
147    precompiles.move_precompiles(moves).map_err(
148        |alloy_evm::precompiles::MovePrecompileError::NotAPrecompile(addr)| {
149            EthSimulateError::NotAPrecompile(addr)
150        },
151    )?;
152
153    Ok(())
154}
155
156/// Converts all [`TransactionRequest`]s into [`Recovered`] transactions and applies them to the
157/// given [`BlockExecutor`].
158///
159/// Returns all executed transactions and the result of the execution.
160///
161/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
162#[expect(clippy::type_complexity)]
163pub fn execute_transactions<S, T>(
164    mut builder: S,
165    calls: Vec<RpcTxReq<T::Network>>,
166    default_gas_limit: u64,
167    chain_id: u64,
168    converter: &T,
169) -> Result<
170    (
171        BlockBuilderOutcome<S::Primitives>,
172        Vec<ExecutionResult<<<S::Executor as BlockExecutor>::Evm as Evm>::HaltReason>>,
173    ),
174    EthApiError,
175>
176where
177    S: BlockBuilder<Executor: BlockExecutor<Evm: Evm<DB: Database<Error: Into<EthApiError>>>>>,
178    T: RpcConvert<Primitives = S::Primitives>,
179{
180    builder.apply_pre_execution_changes()?;
181
182    let mut results = Vec::with_capacity(calls.len());
183    for call in calls {
184        // Resolve transaction, populate missing fields and enforce calls
185        // correctness.
186        let tx = resolve_transaction(
187            call,
188            default_gas_limit,
189            builder.evm().block().basefee(),
190            chain_id,
191            builder.evm_mut().db_mut(),
192            converter,
193        )?;
194        // Create transaction with an empty envelope.
195        // The effect for a layer-2 execution client is that it does not charge L1 cost.
196        let tx = WithEncoded::new(Default::default(), tx);
197
198        builder
199            .execute_transaction_with_result_closure(tx, |result| results.push(result.clone()))?;
200    }
201
202    // Pass noop provider to skip state root calculations.
203    let result = builder.finish(NoopProvider::default())?;
204
205    Ok((result, results))
206}
207
208/// Goes over the list of [`TransactionRequest`]s and populates missing fields trying to resolve
209/// them into primitive transactions.
210///
211/// This will set the defaults as defined in <https://github.com/ethereum/execution-apis/blob/e56d3208789259d0b09fa68e9d8594aa4d73c725/docs/ethsimulatev1-notes.md#default-values-for-transactions>
212///
213/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
214pub fn resolve_transaction<DB: Database, Tx, T>(
215    mut tx: RpcTxReq<T::Network>,
216    default_gas_limit: u64,
217    block_base_fee_per_gas: u64,
218    chain_id: u64,
219    db: &mut DB,
220    converter: &T,
221) -> Result<Recovered<Tx>, EthApiError>
222where
223    DB::Error: Into<EthApiError>,
224    T: RpcConvert<Primitives: NodePrimitives<SignedTx = Tx>>,
225{
226    // If we're missing any fields we try to fill nonce, gas and
227    // gas price.
228    let tx_type = tx.as_ref().output_tx_type();
229
230    let from = if let Some(from) = tx.as_ref().from() {
231        from
232    } else {
233        tx.as_mut().set_from(Address::ZERO);
234        Address::ZERO
235    };
236
237    if tx.as_ref().nonce().is_none() {
238        tx.as_mut().set_nonce(
239            db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default(),
240        );
241    }
242
243    if tx.as_ref().gas_limit().is_none() {
244        tx.as_mut().set_gas_limit(default_gas_limit);
245    }
246
247    if tx.as_ref().chain_id().is_none() {
248        tx.as_mut().set_chain_id(chain_id);
249    }
250
251    if tx.as_ref().kind().is_none() {
252        tx.as_mut().set_kind(TxKind::Create);
253    }
254
255    // if we can't build the _entire_ transaction yet, we need to check the fee values
256    if tx.as_ref().output_tx_type_checked().is_none() {
257        if tx_type.is_legacy() || tx_type.is_eip2930() {
258            if tx.as_ref().gas_price().is_none() {
259                tx.as_mut().set_gas_price(block_base_fee_per_gas as u128);
260            }
261        } else {
262            // set dynamic 1559 fees
263            if tx.as_ref().max_fee_per_gas().is_none() {
264                let mut max_fee_per_gas = block_base_fee_per_gas as u128;
265                if let Some(prio_fee) = tx.as_ref().max_priority_fee_per_gas() {
266                    // if a prio fee is provided we need to select the max fee accordingly
267                    // because the base fee must be higher than the prio fee.
268                    max_fee_per_gas = prio_fee.max(max_fee_per_gas);
269                }
270                tx.as_mut().set_max_fee_per_gas(max_fee_per_gas);
271            }
272            if tx.as_ref().max_priority_fee_per_gas().is_none() {
273                tx.as_mut().set_max_priority_fee_per_gas(0);
274            }
275        }
276    }
277
278    let tx =
279        converter.build_simulate_v1_transaction(tx).map_err(|e| EthApiError::other(e.into()))?;
280
281    Ok(Recovered::new_unchecked(tx, from))
282}
283
284/// Handles outputs of the calls execution and builds a [`SimulatedBlock`].
285pub fn build_simulated_block<Err, T>(
286    block: RecoveredBlock<BlockTy<T::Primitives>>,
287    results: Vec<ExecutionResult<HaltReasonFor<T::Evm>>>,
288    txs_kind: BlockTransactionsKind,
289    converter: &T,
290) -> Result<SimulatedBlock<RpcBlock<T::Network>>, Err>
291where
292    Err: std::error::Error
293        + FromEthApiError
294        + FromEvmError<T::Evm>
295        + From<T::Error>
296        + Into<jsonrpsee_types::ErrorObject<'static>>,
297    T: RpcConvert,
298{
299    let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
300
301    let mut log_index = 0;
302    for (index, (result, tx)) in results.into_iter().zip(block.body().transactions()).enumerate() {
303        let call = match result {
304            ExecutionResult::Halt { reason, gas, .. } => {
305                let error = Err::from_evm_halt(reason, tx.gas_limit());
306                #[allow(clippy::needless_update)]
307                SimCallResult {
308                    return_data: Bytes::new(),
309                    error: Some(SimulateError {
310                        message: error.to_string(),
311                        code: SIMULATE_VM_ERROR_CODE,
312                        ..SimulateError::invalid_params()
313                    }),
314                    gas_used: gas.used(),
315                    logs: Vec::new(),
316                    status: false,
317                    ..Default::default()
318                }
319            }
320            ExecutionResult::Revert { output, gas, .. } => {
321                let error = Err::from_revert(output.clone());
322                #[allow(clippy::needless_update)]
323                SimCallResult {
324                    return_data: output,
325                    error: Some(SimulateError {
326                        message: error.to_string(),
327                        code: SIMULATE_REVERT_CODE,
328                        ..SimulateError::invalid_params()
329                    }),
330                    gas_used: gas.used(),
331                    status: false,
332                    logs: Vec::new(),
333                    ..Default::default()
334                }
335            }
336            ExecutionResult::Success { output, gas, logs, .. } =>
337            {
338                #[allow(clippy::needless_update)]
339                SimCallResult {
340                    return_data: output.into_data(),
341                    error: None,
342                    gas_used: gas.used(),
343                    logs: logs
344                        .into_iter()
345                        .map(|log| {
346                            log_index += 1;
347                            alloy_rpc_types_eth::Log {
348                                inner: log,
349                                log_index: Some(log_index - 1),
350                                transaction_index: Some(index as u64),
351                                transaction_hash: Some(*tx.tx_hash()),
352                                block_hash: Some(block.hash()),
353                                block_number: Some(block.header().number()),
354                                block_timestamp: Some(block.header().timestamp()),
355                                ..Default::default()
356                            }
357                        })
358                        .collect(),
359                    status: true,
360                    ..Default::default()
361                }
362            }
363        };
364
365        calls.push(call);
366    }
367
368    let block = block.into_rpc_block(
369        txs_kind,
370        |tx, tx_info| converter.fill(tx, tx_info),
371        |header, size| converter.convert_header(header, size),
372    )?;
373    Ok(SimulatedBlock { inner: block, calls })
374}