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_interface::result::ExecutionResult,
28    primitives::{Address, Bytes, TxKind},
29    Database,
30};
31
32/// Errors which may occur during `eth_simulateV1` execution.
33#[derive(Debug, thiserror::Error)]
34pub enum EthSimulateError {
35    /// Total gas limit of transactions for the block exceeds the block gas limit.
36    #[error("Block gas limit exceeded by the block's transactions")]
37    BlockGasLimitExceeded,
38    /// Max gas limit for entire operation exceeded.
39    #[error("Client adjustable limit reached")]
40    GasLimitReached,
41}
42
43impl EthSimulateError {
44    const fn error_code(&self) -> i32 {
45        match self {
46            Self::BlockGasLimitExceeded => -38015,
47            Self::GasLimitReached => -38026,
48        }
49    }
50}
51
52impl ToRpcError for EthSimulateError {
53    fn to_rpc_error(&self) -> ErrorObject<'static> {
54        rpc_err(self.error_code(), self.to_string(), None)
55    }
56}
57
58/// Converts all [`TransactionRequest`]s into [`Recovered`] transactions and applies them to the
59/// given [`BlockExecutor`].
60///
61/// Returns all executed transactions and the result of the execution.
62///
63/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
64#[expect(clippy::type_complexity)]
65pub fn execute_transactions<S, T>(
66    mut builder: S,
67    calls: Vec<RpcTxReq<T::Network>>,
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: RpcConvert<Primitives = 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>
114///
115/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
116pub fn resolve_transaction<DB: Database, Tx, T>(
117    mut tx: RpcTxReq<T::Network>,
118    default_gas_limit: u64,
119    block_base_fee_per_gas: u64,
120    chain_id: u64,
121    db: &mut DB,
122    tx_resp_builder: &T,
123) -> Result<Recovered<Tx>, EthApiError>
124where
125    DB::Error: Into<EthApiError>,
126    T: RpcConvert<Primitives: NodePrimitives<SignedTx = Tx>>,
127{
128    // If we're missing any fields we try to fill nonce, gas and
129    // gas price.
130    let tx_type = tx.as_ref().output_tx_type();
131
132    let from = if let Some(from) = tx.as_ref().from() {
133        from
134    } else {
135        tx.as_mut().set_from(Address::ZERO);
136        Address::ZERO
137    };
138
139    if tx.as_ref().nonce().is_none() {
140        tx.as_mut().set_nonce(
141            db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default(),
142        );
143    }
144
145    if tx.as_ref().gas_limit().is_none() {
146        tx.as_mut().set_gas_limit(default_gas_limit);
147    }
148
149    if tx.as_ref().chain_id().is_none() {
150        tx.as_mut().set_chain_id(chain_id);
151    }
152
153    if tx.as_ref().kind().is_none() {
154        tx.as_mut().set_kind(TxKind::Create);
155    }
156
157    // if we can't build the _entire_ transaction yet, we need to check the fee values
158    if tx.as_ref().output_tx_type_checked().is_none() {
159        if tx_type.is_legacy() || tx_type.is_eip2930() {
160            if tx.as_ref().gas_price().is_none() {
161                tx.as_mut().set_gas_price(block_base_fee_per_gas as u128);
162            }
163        } else {
164            // set dynamic 1559 fees
165            if tx.as_ref().max_fee_per_gas().is_none() {
166                let mut max_fee_per_gas = block_base_fee_per_gas as u128;
167                if let Some(prio_fee) = tx.as_ref().max_priority_fee_per_gas() {
168                    // if a prio fee is provided we need to select the max fee accordingly
169                    // because the base fee must be higher than the prio fee.
170                    max_fee_per_gas = prio_fee.max(max_fee_per_gas);
171                }
172                tx.as_mut().set_max_fee_per_gas(max_fee_per_gas);
173            }
174            if tx.as_ref().max_priority_fee_per_gas().is_none() {
175                tx.as_mut().set_max_priority_fee_per_gas(0);
176            }
177        }
178    }
179
180    let tx = tx_resp_builder
181        .build_simulate_v1_transaction(tx)
182        .map_err(|e| EthApiError::other(e.into()))?;
183
184    Ok(Recovered::new_unchecked(tx, from))
185}
186
187/// Handles outputs of the calls execution and builds a [`SimulatedBlock`].
188pub fn build_simulated_block<T, Halt: Clone>(
189    block: RecoveredBlock<BlockTy<T::Primitives>>,
190    results: Vec<ExecutionResult<Halt>>,
191    txs_kind: BlockTransactionsKind,
192    tx_resp_builder: &T,
193) -> Result<SimulatedBlock<RpcBlock<T::Network>>, T::Error>
194where
195    T: RpcConvert<Error: FromEthApiError + FromEvmHalt<Halt>>,
196{
197    let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
198
199    let mut log_index = 0;
200    for (index, (result, tx)) in results.into_iter().zip(block.body().transactions()).enumerate() {
201        let call = match result {
202            ExecutionResult::Halt { reason, gas_used } => {
203                let error = T::Error::from_evm_halt(reason, tx.gas_limit());
204                SimCallResult {
205                    return_data: Bytes::new(),
206                    error: Some(SimulateError {
207                        message: error.to_string(),
208                        code: error.into().code(),
209                    }),
210                    gas_used,
211                    logs: Vec::new(),
212                    status: false,
213                }
214            }
215            ExecutionResult::Revert { output, gas_used } => {
216                let error = RevertError::new(output.clone());
217                SimCallResult {
218                    return_data: output,
219                    error: Some(SimulateError {
220                        code: error.error_code(),
221                        message: error.to_string(),
222                    }),
223                    gas_used,
224                    status: false,
225                    logs: Vec::new(),
226                }
227            }
228            ExecutionResult::Success { output, gas_used, logs, .. } => SimCallResult {
229                return_data: output.into_data(),
230                error: None,
231                gas_used,
232                logs: logs
233                    .into_iter()
234                    .map(|log| {
235                        log_index += 1;
236                        alloy_rpc_types_eth::Log {
237                            inner: log,
238                            log_index: Some(log_index - 1),
239                            transaction_index: Some(index as u64),
240                            transaction_hash: Some(*tx.tx_hash()),
241                            block_number: Some(block.header().number()),
242                            block_timestamp: Some(block.header().timestamp()),
243                            ..Default::default()
244                        }
245                    })
246                    .collect(),
247                status: true,
248            },
249        };
250
251        calls.push(call);
252    }
253
254    let block = block.into_rpc_block(
255        txs_kind,
256        |tx, tx_info| tx_resp_builder.fill(tx, tx_info),
257        |header, size| tx_resp_builder.convert_header(header, size),
258    )?;
259    Ok(SimulatedBlock { inner: block, calls })
260}