reth_rpc_eth_types/
simulate.rs

1//! Utilities for serving `eth_simulateV1`
2
3use crate::{
4    error::{
5        api::{FromEthApiError, FromEvmHalt},
6        ToRpcError,
7    },
8    EthApiError, RevertError,
9};
10use alloy_consensus::{BlockHeader, Transaction as _, TxType};
11use alloy_eips::eip2718::WithEncoded;
12use alloy_rpc_types_eth::{
13    simulate::{SimCallResult, SimulateError, SimulatedBlock},
14    transaction::TransactionRequest,
15    Block, BlockTransactionsKind, Header,
16};
17use jsonrpsee_types::ErrorObject;
18use reth_evm::{
19    execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
20    Evm,
21};
22use reth_primitives_traits::{
23    block::BlockTx, BlockBody as _, Recovered, RecoveredBlock, SignedTransaction, TxTy,
24};
25use reth_rpc_server_types::result::rpc_err;
26use reth_rpc_types_compat::{block::from_block, TransactionCompat};
27use reth_storage_api::noop::NoopProvider;
28use revm::{
29    context_interface::result::ExecutionResult,
30    primitives::{Address, Bytes, TxKind},
31    Database,
32};
33
34/// Errors which may occur during `eth_simulateV1` execution.
35#[derive(Debug, thiserror::Error)]
36pub enum EthSimulateError {
37    /// Total gas limit of transactions for the block exceeds the block gas limit.
38    #[error("Block gas limit exceeded by the block's transactions")]
39    BlockGasLimitExceeded,
40    /// Max gas limit for entire operation exceeded.
41    #[error("Client adjustable limit reached")]
42    GasLimitReached,
43}
44
45impl EthSimulateError {
46    const fn error_code(&self) -> i32 {
47        match self {
48            Self::BlockGasLimitExceeded => -38015,
49            Self::GasLimitReached => -38026,
50        }
51    }
52}
53
54impl ToRpcError for EthSimulateError {
55    fn to_rpc_error(&self) -> ErrorObject<'static> {
56        rpc_err(self.error_code(), self.to_string(), None)
57    }
58}
59
60/// Converts all [`TransactionRequest`]s into [`Recovered`] transactions and applies them to the
61/// given [`BlockExecutor`].
62///
63/// Returns all executed transactions and the result of the execution.
64#[expect(clippy::type_complexity)]
65pub fn execute_transactions<S, T>(
66    mut builder: S,
67    calls: Vec<TransactionRequest>,
68    default_gas_limit: u64,
69    chain_id: u64,
70    tx_resp_builder: &T,
71) -> Result<
72    (
73        BlockBuilderOutcome<S::Primitives>,
74        Vec<ExecutionResult<<<S::Executor as BlockExecutor>::Evm as Evm>::HaltReason>>,
75    ),
76    EthApiError,
77>
78where
79    S: BlockBuilder<Executor: BlockExecutor<Evm: Evm<DB: Database<Error: Into<EthApiError>>>>>,
80    T: TransactionCompat<TxTy<S::Primitives>>,
81{
82    builder.apply_pre_execution_changes()?;
83
84    let mut results = Vec::with_capacity(calls.len());
85    for call in calls {
86        // Resolve transaction, populate missing fields and enforce calls
87        // correctness.
88        let tx = resolve_transaction(
89            call,
90            default_gas_limit,
91            builder.evm().block().basefee,
92            chain_id,
93            builder.evm_mut().db_mut(),
94            tx_resp_builder,
95        )?;
96        // Create transaction with an empty envelope.
97        // The effect for a layer-2 execution client is that it does not charge L1 cost.
98        let tx = WithEncoded::new(Default::default(), tx);
99
100        builder
101            .execute_transaction_with_result_closure(tx, |result| results.push(result.clone()))?;
102    }
103
104    // Pass noop provider to skip state root calculations.
105    let result = builder.finish(NoopProvider::default())?;
106
107    Ok((result, results))
108}
109
110/// Goes over the list of [`TransactionRequest`]s and populates missing fields trying to resolve
111/// them into primitive transactions.
112///
113/// This will set the defaults as defined in <https://github.com/ethereum/execution-apis/blob/e56d3208789259d0b09fa68e9d8594aa4d73c725/docs/ethsimulatev1-notes.md#default-values-for-transactions>
114pub fn resolve_transaction<DB: Database, Tx, T: TransactionCompat<Tx>>(
115    mut tx: TransactionRequest,
116    default_gas_limit: u64,
117    block_base_fee_per_gas: u64,
118    chain_id: u64,
119    db: &mut DB,
120    tx_resp_builder: &T,
121) -> Result<Recovered<Tx>, EthApiError>
122where
123    DB::Error: Into<EthApiError>,
124{
125    // If we're missing any fields we try to fill nonce, gas and
126    // gas price.
127    let tx_type = tx.preferred_type();
128
129    let from = if let Some(from) = tx.from {
130        from
131    } else {
132        tx.from = Some(Address::ZERO);
133        Address::ZERO
134    };
135
136    if tx.nonce.is_none() {
137        tx.nonce =
138            Some(db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default());
139    }
140
141    if tx.gas.is_none() {
142        tx.gas = Some(default_gas_limit);
143    }
144
145    if tx.chain_id.is_none() {
146        tx.chain_id = Some(chain_id);
147    }
148
149    if tx.to.is_none() {
150        tx.to = Some(TxKind::Create);
151    }
152
153    // if we can't build the _entire_ transaction yet, we need to check the fee values
154    if tx.buildable_type().is_none() {
155        match tx_type {
156            TxType::Legacy | TxType::Eip2930 => {
157                if tx.gas_price.is_none() {
158                    tx.gas_price = Some(block_base_fee_per_gas as u128);
159                }
160            }
161            _ => {
162                // set dynamic 1559 fees
163                if tx.max_fee_per_gas.is_none() {
164                    let mut max_fee_per_gas = block_base_fee_per_gas as u128;
165                    if let Some(prio_fee) = tx.max_priority_fee_per_gas {
166                        // if a prio fee is provided we need to select the max fee accordingly
167                        // because the base fee must be higher than the prio fee.
168                        max_fee_per_gas = prio_fee.max(max_fee_per_gas);
169                    }
170                    tx.max_fee_per_gas = Some(max_fee_per_gas);
171                }
172                if tx.max_priority_fee_per_gas.is_none() {
173                    tx.max_priority_fee_per_gas = Some(0);
174                }
175            }
176        }
177    }
178
179    let tx = tx_resp_builder
180        .build_simulate_v1_transaction(tx)
181        .map_err(|e| EthApiError::other(e.into()))?;
182
183    Ok(Recovered::new_unchecked(tx, from))
184}
185
186/// Handles outputs of the calls execution and builds a [`SimulatedBlock`].
187#[expect(clippy::type_complexity)]
188pub fn build_simulated_block<T, B, Halt: Clone>(
189    block: RecoveredBlock<B>,
190    results: Vec<ExecutionResult<Halt>>,
191    full_transactions: bool,
192    tx_resp_builder: &T,
193) -> Result<SimulatedBlock<Block<T::Transaction, Header<B::Header>>>, T::Error>
194where
195    T: TransactionCompat<BlockTx<B>, Error: FromEthApiError + FromEvmHalt<Halt>>,
196    B: reth_primitives_traits::Block,
197{
198    let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
199
200    let mut log_index = 0;
201    for (index, (result, tx)) in results.iter().zip(block.body().transactions()).enumerate() {
202        let call = match result {
203            ExecutionResult::Halt { reason, gas_used } => {
204                let error = T::Error::from_evm_halt(reason.clone(), tx.gas_limit());
205                SimCallResult {
206                    return_data: Bytes::new(),
207                    error: Some(SimulateError {
208                        message: error.to_string(),
209                        code: error.into().code(),
210                    }),
211                    gas_used: *gas_used,
212                    logs: Vec::new(),
213                    status: false,
214                }
215            }
216            ExecutionResult::Revert { output, gas_used } => {
217                let error = RevertError::new(output.clone());
218                SimCallResult {
219                    return_data: output.clone(),
220                    error: Some(SimulateError {
221                        code: error.error_code(),
222                        message: error.to_string(),
223                    }),
224                    gas_used: *gas_used,
225                    status: false,
226                    logs: Vec::new(),
227                }
228            }
229            ExecutionResult::Success { output, gas_used, logs, .. } => SimCallResult {
230                return_data: output.clone().into_data(),
231                error: None,
232                gas_used: *gas_used,
233                logs: logs
234                    .iter()
235                    .map(|log| {
236                        log_index += 1;
237                        alloy_rpc_types_eth::Log {
238                            inner: log.clone(),
239                            log_index: Some(log_index - 1),
240                            transaction_index: Some(index as u64),
241                            transaction_hash: Some(*tx.tx_hash()),
242                            block_number: Some(block.header().number()),
243                            block_timestamp: Some(block.header().timestamp()),
244                            ..Default::default()
245                        }
246                    })
247                    .collect(),
248                status: true,
249            },
250        };
251
252        calls.push(call);
253    }
254
255    let txs_kind =
256        if full_transactions { BlockTransactionsKind::Full } else { BlockTransactionsKind::Hashes };
257
258    let block = from_block(block, txs_kind, tx_resp_builder)?;
259    Ok(SimulatedBlock { inner: block, calls })
260}