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