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