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 _};
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::{
23    BlockBody as _, BlockTy, NodePrimitives, Recovered, RecoveredBlock, SignedTransaction,
24};
25use reth_rpc_convert::{RpcBlock, RpcConvert, RpcTxReq};
26use reth_rpc_server_types::result::rpc_err;
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///
65/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
66#[expect(clippy::type_complexity)]
67pub fn execute_transactions<S, T>(
68    mut builder: S,
69    calls: Vec<RpcTxReq<T::Network>>,
70    default_gas_limit: u64,
71    chain_id: u64,
72    tx_resp_builder: &T,
73) -> Result<
74    (
75        BlockBuilderOutcome<S::Primitives>,
76        Vec<ExecutionResult<<<S::Executor as BlockExecutor>::Evm as Evm>::HaltReason>>,
77    ),
78    EthApiError,
79>
80where
81    S: BlockBuilder<Executor: BlockExecutor<Evm: Evm<DB: Database<Error: Into<EthApiError>>>>>,
82    T: RpcConvert<Primitives = S::Primitives>,
83{
84    builder.apply_pre_execution_changes()?;
85
86    let mut results = Vec::with_capacity(calls.len());
87    for call in calls {
88        // Resolve transaction, populate missing fields and enforce calls
89        // correctness.
90        let tx = resolve_transaction(
91            call,
92            default_gas_limit,
93            builder.evm().block().basefee,
94            chain_id,
95            builder.evm_mut().db_mut(),
96            tx_resp_builder,
97        )?;
98        // Create transaction with an empty envelope.
99        // The effect for a layer-2 execution client is that it does not charge L1 cost.
100        let tx = WithEncoded::new(Default::default(), tx);
101
102        builder
103            .execute_transaction_with_result_closure(tx, |result| results.push(result.clone()))?;
104    }
105
106    // Pass noop provider to skip state root calculations.
107    let result = builder.finish(NoopProvider::default())?;
108
109    Ok((result, results))
110}
111
112/// Goes over the list of [`TransactionRequest`]s and populates missing fields trying to resolve
113/// them into primitive transactions.
114///
115/// This will set the defaults as defined in <https://github.com/ethereum/execution-apis/blob/e56d3208789259d0b09fa68e9d8594aa4d73c725/docs/ethsimulatev1-notes.md#default-values-for-transactions>
116///
117/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
118pub fn resolve_transaction<DB: Database, Tx, T>(
119    mut tx: RpcTxReq<T::Network>,
120    default_gas_limit: u64,
121    block_base_fee_per_gas: u64,
122    chain_id: u64,
123    db: &mut DB,
124    tx_resp_builder: &T,
125) -> Result<Recovered<Tx>, EthApiError>
126where
127    DB::Error: Into<EthApiError>,
128    T: RpcConvert<Primitives: NodePrimitives<SignedTx = Tx>>,
129{
130    // If we're missing any fields we try to fill nonce, gas and
131    // gas price.
132    let tx_type = tx.as_ref().output_tx_type();
133
134    let from = if let Some(from) = tx.as_ref().from() {
135        from
136    } else {
137        tx.as_mut().set_from(Address::ZERO);
138        Address::ZERO
139    };
140
141    if tx.as_ref().nonce().is_none() {
142        tx.as_mut().set_nonce(
143            db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default(),
144        );
145    }
146
147    if tx.as_ref().gas_limit().is_none() {
148        tx.as_mut().set_gas_limit(default_gas_limit);
149    }
150
151    if tx.as_ref().chain_id().is_none() {
152        tx.as_mut().set_chain_id(chain_id);
153    }
154
155    if tx.as_ref().kind().is_none() {
156        tx.as_mut().set_kind(TxKind::Create);
157    }
158
159    // if we can't build the _entire_ transaction yet, we need to check the fee values
160    if tx.as_ref().output_tx_type_checked().is_none() {
161        if tx_type.is_legacy() || tx_type.is_eip2930() {
162            if tx.as_ref().gas_price().is_none() {
163                tx.as_mut().set_gas_price(block_base_fee_per_gas as u128);
164            }
165        } else {
166            // set dynamic 1559 fees
167            if tx.as_ref().max_fee_per_gas().is_none() {
168                let mut max_fee_per_gas = block_base_fee_per_gas as u128;
169                if let Some(prio_fee) = tx.as_ref().max_priority_fee_per_gas() {
170                    // if a prio fee is provided we need to select the max fee accordingly
171                    // because the base fee must be higher than the prio fee.
172                    max_fee_per_gas = prio_fee.max(max_fee_per_gas);
173                }
174                tx.as_mut().set_max_fee_per_gas(max_fee_per_gas);
175            }
176            if tx.as_ref().max_priority_fee_per_gas().is_none() {
177                tx.as_mut().set_max_priority_fee_per_gas(0);
178            }
179        }
180    }
181
182    let tx = tx_resp_builder
183        .build_simulate_v1_transaction(tx)
184        .map_err(|e| EthApiError::other(e.into()))?;
185
186    Ok(Recovered::new_unchecked(tx, from))
187}
188
189/// Handles outputs of the calls execution and builds a [`SimulatedBlock`].
190pub fn build_simulated_block<T, Halt: Clone>(
191    block: RecoveredBlock<BlockTy<T::Primitives>>,
192    results: Vec<ExecutionResult<Halt>>,
193    txs_kind: BlockTransactionsKind,
194    tx_resp_builder: &T,
195) -> Result<SimulatedBlock<RpcBlock<T::Network>>, T::Error>
196where
197    T: RpcConvert<Error: FromEthApiError + FromEvmHalt<Halt>>,
198{
199    let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
200
201    let mut log_index = 0;
202    for (index, (result, tx)) in results.into_iter().zip(block.body().transactions()).enumerate() {
203        let call = match result {
204            ExecutionResult::Halt { reason, gas_used } => {
205                let error = T::Error::from_evm_halt(reason, tx.gas_limit());
206                SimCallResult {
207                    return_data: Bytes::new(),
208                    error: Some(SimulateError {
209                        message: error.to_string(),
210                        code: error.into().code(),
211                    }),
212                    gas_used,
213                    logs: Vec::new(),
214                    status: false,
215                }
216            }
217            ExecutionResult::Revert { output, gas_used } => {
218                let error = RevertError::new(output.clone());
219                SimCallResult {
220                    return_data: output,
221                    error: Some(SimulateError {
222                        code: error.error_code(),
223                        message: error.to_string(),
224                    }),
225                    gas_used,
226                    status: false,
227                    logs: Vec::new(),
228                }
229            }
230            ExecutionResult::Success { output, gas_used, logs, .. } => SimCallResult {
231                return_data: output.into_data(),
232                error: None,
233                gas_used,
234                logs: logs
235                    .into_iter()
236                    .map(|log| {
237                        log_index += 1;
238                        alloy_rpc_types_eth::Log {
239                            inner: log,
240                            log_index: Some(log_index - 1),
241                            transaction_index: Some(index as u64),
242                            transaction_hash: Some(*tx.tx_hash()),
243                            block_number: Some(block.header().number()),
244                            block_timestamp: Some(block.header().timestamp()),
245                            ..Default::default()
246                        }
247                    })
248                    .collect(),
249                status: true,
250            },
251        };
252
253        calls.push(call);
254    }
255
256    let block = block.into_rpc_block(
257        txs_kind,
258        |tx, tx_info| tx_resp_builder.fill(tx, tx_info),
259        |header, size| tx_resp_builder.convert_header(header, size),
260    )?;
261    Ok(SimulatedBlock { inner: block, calls })
262}