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