reth_rpc_eth_types/
simulate.rs

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