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    /// 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
202            .execute_transaction_with_result_closure(tx, |result| results.push(result.clone()))?;
203    }
204
205    // Pass noop provider to skip state root calculations.
206    let result = builder.finish(NoopProvider::default())?;
207
208    Ok((result, results))
209}
210
211/// Goes over the list of [`TransactionRequest`]s and populates missing fields trying to resolve
212/// them into primitive transactions.
213///
214/// This will set the defaults as defined in <https://github.com/ethereum/execution-apis/blob/e56d3208789259d0b09fa68e9d8594aa4d73c725/docs/ethsimulatev1-notes.md#default-values-for-transactions>
215///
216/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
217pub fn resolve_transaction<DB: Database, Tx, T>(
218    mut tx: RpcTxReq<T::Network>,
219    default_gas_limit: u64,
220    block_base_fee_per_gas: u64,
221    chain_id: u64,
222    db: &mut DB,
223    converter: &T,
224) -> Result<Recovered<Tx>, EthApiError>
225where
226    DB::Error: Into<EthApiError>,
227    T: RpcConvert<Primitives: NodePrimitives<SignedTx = Tx>>,
228{
229    // If we're missing any fields we try to fill nonce, gas and
230    // gas price.
231    let tx_type = tx.as_ref().output_tx_type();
232
233    let from = if let Some(from) = tx.as_ref().from() {
234        from
235    } else {
236        tx.as_mut().set_from(Address::ZERO);
237        Address::ZERO
238    };
239
240    if tx.as_ref().nonce().is_none() {
241        tx.as_mut().set_nonce(
242            db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default(),
243        );
244    }
245
246    if tx.as_ref().gas_limit().is_none() {
247        tx.as_mut().set_gas_limit(default_gas_limit);
248    }
249
250    if tx.as_ref().chain_id().is_none() {
251        tx.as_mut().set_chain_id(chain_id);
252    }
253
254    if tx.as_ref().kind().is_none() {
255        tx.as_mut().set_kind(TxKind::Create);
256    }
257
258    // if we can't build the _entire_ transaction yet, we need to check the fee values
259    if tx.as_ref().output_tx_type_checked().is_none() {
260        if tx_type.is_legacy() || tx_type.is_eip2930() {
261            if tx.as_ref().gas_price().is_none() {
262                tx.as_mut().set_gas_price(block_base_fee_per_gas as u128);
263            }
264        } else {
265            // set dynamic 1559 fees
266            if tx.as_ref().max_fee_per_gas().is_none() {
267                let mut max_fee_per_gas = block_base_fee_per_gas as u128;
268                if let Some(prio_fee) = tx.as_ref().max_priority_fee_per_gas() {
269                    // if a prio fee is provided we need to select the max fee accordingly
270                    // because the base fee must be higher than the prio fee.
271                    max_fee_per_gas = prio_fee.max(max_fee_per_gas);
272                }
273                tx.as_mut().set_max_fee_per_gas(max_fee_per_gas);
274            }
275            if tx.as_ref().max_priority_fee_per_gas().is_none() {
276                tx.as_mut().set_max_priority_fee_per_gas(0);
277            }
278        }
279    }
280
281    let tx =
282        converter.build_simulate_v1_transaction(tx).map_err(|e| EthApiError::other(e.into()))?;
283
284    Ok(Recovered::new_unchecked(tx, from))
285}
286
287/// Handles outputs of the calls execution and builds a [`SimulatedBlock`].
288pub fn build_simulated_block<Err, T>(
289    block: RecoveredBlock<BlockTy<T::Primitives>>,
290    results: Vec<ExecutionResult<HaltReasonFor<T::Evm>>>,
291    txs_kind: BlockTransactionsKind,
292    converter: &T,
293) -> Result<SimulatedBlock<RpcBlock<T::Network>>, Err>
294where
295    Err: std::error::Error
296        + FromEthApiError
297        + FromEvmError<T::Evm>
298        + From<T::Error>
299        + Into<jsonrpsee_types::ErrorObject<'static>>,
300    T: RpcConvert,
301{
302    let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
303
304    let mut log_index = 0;
305    for (index, (result, tx)) in results.into_iter().zip(block.body().transactions()).enumerate() {
306        let call = match result {
307            ExecutionResult::Halt { reason, gas, .. } => {
308                let error = Err::from_evm_halt(reason, tx.gas_limit());
309                #[allow(clippy::needless_update)]
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                #[allow(clippy::needless_update)]
326                SimCallResult {
327                    return_data: output,
328                    error: Some(SimulateError {
329                        message: error.to_string(),
330                        code: SIMULATE_REVERT_CODE,
331                        ..SimulateError::invalid_params()
332                    }),
333                    gas_used: gas.used(),
334                    status: false,
335                    logs: Vec::new(),
336                    ..Default::default()
337                }
338            }
339            ExecutionResult::Success { output, gas, logs, .. } =>
340            {
341                #[allow(clippy::needless_update)]
342                SimCallResult {
343                    return_data: output.into_data(),
344                    error: None,
345                    gas_used: gas.used(),
346                    logs: logs
347                        .into_iter()
348                        .map(|log| {
349                            log_index += 1;
350                            alloy_rpc_types_eth::Log {
351                                inner: log,
352                                log_index: Some(log_index - 1),
353                                transaction_index: Some(index as u64),
354                                transaction_hash: Some(*tx.tx_hash()),
355                                block_hash: Some(block.hash()),
356                                block_number: Some(block.header().number()),
357                                block_timestamp: Some(block.header().timestamp()),
358                                ..Default::default()
359                            }
360                        })
361                        .collect(),
362                    status: true,
363                    ..Default::default()
364                }
365            }
366        };
367
368        calls.push(call);
369    }
370
371    let block = block.into_rpc_block(
372        txs_kind,
373        |tx, tx_info| converter.fill(tx, tx_info),
374        |header, size| converter.convert_header(header, size),
375    )?;
376    Ok(SimulatedBlock { inner: block, calls })
377}