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