Skip to main content

reth_rpc_eth_types/
simulate.rs

1//! Utilities for serving `eth_simulateV1`
2
3use crate::{
4    error::{api::FromEthApiError, FromEvmError, ToRpcError},
5    EthApiError,
6};
7use alloy_chains::Chain;
8use alloy_consensus::{transaction::TxHashRef, BlockHeader, Transaction as _};
9use alloy_eips::eip2718::WithEncoded;
10use alloy_evm::{block::TxResult, precompiles::PrecompilesMap};
11use alloy_network::{NetworkTransactionBuilder, TransactionBuilder};
12use alloy_rpc_types_eth::{
13    simulate::{SimBlock, SimCallResult, SimulateError, SimulatedBlock},
14    state::StateOverride,
15    BlockId, BlockOverrides, BlockTransactionsKind,
16};
17use jsonrpsee_types::{error::INTERNAL_ERROR_CODE, ErrorObject};
18use reth_evm::{
19    execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
20    Evm, HaltReasonFor,
21};
22use reth_primitives_traits::{
23    BlockBody as _, BlockTy, NodePrimitives, Recovered, RecoveredBlock, SealedHeader,
24};
25use reth_rpc_convert::{RpcBlock, RpcConvert, RpcTxReq};
26use reth_rpc_server_types::result::rpc_err;
27use reth_storage_api::{noop::NoopProvider, StateProvider};
28use revm::{
29    context::{Block, Cfg as _},
30    context_interface::result::ExecutionResult,
31    primitives::{hardfork::SpecId, Address, Bytes, TxKind, U256},
32    Database,
33};
34
35/// Fallback seconds added between simulated block timestamps when neither the user nor the chain
36/// hint provides a value.
37const SIMULATE_FALLBACK_TIMESTAMP_INCREMENT: u64 = 12;
38
39/// Error code for execution reverted in `eth_simulateV1`.
40///
41/// Consistent with `eth_call` revert error code.
42///
43/// <https://github.com/ethereum/execution-apis/pull/748>
44pub const SIMULATE_REVERT_CODE: i32 = 3;
45
46/// Error code for VM execution errors (e.g., out of gas) in `eth_simulateV1`.
47///
48/// <https://github.com/ethereum/execution-apis>
49pub const SIMULATE_VM_ERROR_CODE: i32 = -32015;
50
51/// Errors which may occur during `eth_simulateV1` execution.
52#[derive(Debug, thiserror::Error)]
53pub enum EthSimulateError {
54    /// Total gas limit of transactions for the block exceeds the block gas limit.
55    #[error("Block gas limit exceeded by the block's transactions")]
56    BlockGasLimitExceeded,
57    /// Number of simulated blocks exceeds the configured client limit.
58    #[error("too many blocks")]
59    TooManyBlocks,
60    /// Max gas limit for entire operation exceeded.
61    #[error("Client adjustable limit reached")]
62    GasLimitReached,
63    /// Base block for the simulation was not found.
64    #[error("block not found: {block}")]
65    BlockNotFound {
66        /// The block id that was requested.
67        block: BlockId,
68    },
69    /// Block number in sequence did not increase.
70    #[error("block numbers must be in order: {got} <= {parent}")]
71    BlockNumberInvalid {
72        /// The block number that was provided.
73        got: u64,
74        /// The parent block number.
75        parent: u64,
76    },
77    /// Block timestamp in sequence did not increase.
78    #[error("block timestamps must be in order: {got} <= {parent}")]
79    BlockTimestampInvalid {
80        /// The block timestamp that was provided.
81        got: u64,
82        /// The parent block timestamp.
83        parent: u64,
84    },
85    /// Transaction nonce is too low.
86    #[error("nonce too low: next nonce {state}, tx nonce {tx}")]
87    NonceTooLow {
88        /// Transaction nonce.
89        tx: u64,
90        /// Current state nonce.
91        state: u64,
92    },
93    /// Transaction nonce is too high.
94    #[error("nonce too high")]
95    NonceTooHigh,
96    /// Transaction nonce cannot be incremented.
97    #[error("nonce has max value")]
98    NonceMaxValue,
99    /// Transaction's baseFeePerGas is too low.
100    #[error("max fee per gas less than block base fee")]
101    BaseFeePerGasTooLow,
102    /// Not enough gas provided to pay for intrinsic gas.
103    #[error("intrinsic gas too low")]
104    IntrinsicGasTooLow,
105    /// Insufficient funds to pay for gas fees and value.
106    #[error("insufficient funds for gas * price + value: have {balance} want {cost}")]
107    InsufficientFunds {
108        /// Transaction cost.
109        cost: U256,
110        /// Sender balance.
111        balance: U256,
112    },
113    /// Sender is not an EOA.
114    #[error("sender is not an EOA")]
115    SenderNotEOA,
116    /// Max init code size exceeded.
117    #[error("max initcode size exceeded")]
118    MaxInitCodeSizeExceeded,
119    /// Attempted to move a non-precompile address.
120    #[error("account {0} is not a precompile")]
121    NotAPrecompile(Address),
122    /// Attempted to move a precompile to its own address.
123    #[error("cannot move precompile {0} to itself")]
124    MovePrecompileToSelf(Address),
125}
126
127impl EthSimulateError {
128    /// Returns the JSON-RPC error code for a `eth_simulateV1` error.
129    pub const fn error_code(&self) -> i32 {
130        match self {
131            Self::NonceTooLow { .. } => -38010,
132            Self::NonceTooHigh => -38011,
133            Self::NonceMaxValue => INTERNAL_ERROR_CODE,
134            Self::BaseFeePerGasTooLow => -38012,
135            Self::IntrinsicGasTooLow => -38013,
136            Self::InsufficientFunds { .. } => -38014,
137            Self::BlockGasLimitExceeded => -38015,
138            Self::BlockNumberInvalid { .. } => -38020,
139            Self::BlockTimestampInvalid { .. } => -38021,
140            Self::SenderNotEOA => -38024,
141            Self::MaxInitCodeSizeExceeded => -38025,
142            Self::TooManyBlocks | Self::GasLimitReached => -38026,
143            Self::MovePrecompileToSelf(_) => -38022,
144            Self::BlockNotFound { .. } | Self::NotAPrecompile(_) => -32000,
145        }
146    }
147}
148
149impl ToRpcError for EthSimulateError {
150    fn to_rpc_error(&self) -> ErrorObject<'static> {
151        rpc_err(self.error_code(), self.to_string(), None)
152    }
153}
154
155/// Sanitizes and gap-fills the chain of [`SimBlock`]s for `eth_simulateV1`.
156///
157/// Walks the provided block-state calls in order and:
158/// - validates that each block number and timestamp strictly increases relative to the parent and
159///   prior simulated block;
160/// - inserts empty filler blocks for every gap in block numbers, so a request like `[block at
161///   number N + k]` over a parent at `N` expands to `k - 1` empty blocks followed by the requested
162///   one (per the execution-apis spec: "If the block number is increased more than `1` compared to
163///   the previous block, new empty blocks are generated in between.");
164/// - assigns default block numbers (`prev_number + 1`) and timestamps (`prev_timestamp + chain
165///   block time`) when missing, so every returned entry has explicit `number` and `time` overrides.
166///   The block time defaults to [`Chain::average_blocktime_hint`] for the chain id, falling back to
167///   `SIMULATE_FALLBACK_TIMESTAMP_INCREMENT` when no hint is registered. Sub-second chain hints are
168///   rounded up because block timestamps are second-granular;
169/// - enforces the global `max_simulate_blocks` cap on the total number of blocks (including
170///   generated fillers).
171pub fn sanitize_chain<TxReq, H>(
172    blocks: Vec<SimBlock<TxReq>>,
173    parent: &SealedHeader<H>,
174    chain_id: u64,
175    max_simulate_blocks: u64,
176) -> Result<Vec<SimBlock<TxReq>>, EthApiError>
177where
178    H: BlockHeader,
179{
180    let timestamp_increment = Chain::from(chain_id)
181        .average_blocktime_hint()
182        .map(|d| d.as_secs().saturating_add(u64::from(d.subsec_nanos() > 0)))
183        .filter(|&s| s > 0)
184        .unwrap_or(SIMULATE_FALLBACK_TIMESTAMP_INCREMENT);
185
186    let mut out = Vec::with_capacity(blocks.len());
187    let base_number = parent.number();
188    let mut prev_number = base_number;
189    let mut prev_timestamp = parent.timestamp();
190
191    for mut block in blocks {
192        let overrides = block.block_overrides.get_or_insert_with(BlockOverrides::default);
193
194        // Default block number to prev + 1 if not specified.
195        let target_number = if let Some(n) = overrides.number {
196            u64::try_from(n).unwrap_or(u64::MAX)
197        } else {
198            let n = prev_number.saturating_add(1);
199            overrides.number = Some(U256::from(n));
200            n
201        };
202
203        if target_number <= prev_number {
204            return Err(EthApiError::other(EthSimulateError::BlockNumberInvalid {
205                got: target_number,
206                parent: prev_number,
207            }));
208        }
209
210        if target_number.saturating_sub(base_number) > max_simulate_blocks {
211            return Err(EthApiError::other(EthSimulateError::TooManyBlocks));
212        }
213
214        // Insert empty filler blocks for any gap between prev_number and target_number.
215        let gap = target_number - prev_number;
216        if gap > 1 {
217            for i in 1..gap {
218                let filler_number = prev_number + i;
219                let filler_time =
220                    prev_timestamp.checked_add(timestamp_increment).ok_or_else(|| {
221                        EthApiError::other(EthSimulateError::BlockTimestampInvalid {
222                            got: prev_timestamp,
223                            parent: prev_timestamp,
224                        })
225                    })?;
226                out.push(SimBlock {
227                    block_overrides: Some(BlockOverrides {
228                        number: Some(U256::from(filler_number)),
229                        time: Some(filler_time),
230                        ..Default::default()
231                    }),
232                    state_overrides: None,
233                    calls: Vec::new(),
234                });
235                prev_timestamp = filler_time;
236            }
237        }
238
239        prev_number = target_number;
240        // Default timestamp to prev + increment if not specified, otherwise validate ordering.
241        let block_time = if let Some(t) = overrides.time {
242            if t <= prev_timestamp {
243                return Err(EthApiError::other(EthSimulateError::BlockTimestampInvalid {
244                    got: t,
245                    parent: prev_timestamp,
246                }));
247            }
248            t
249        } else {
250            let t = prev_timestamp.checked_add(timestamp_increment).ok_or_else(|| {
251                EthApiError::other(EthSimulateError::BlockTimestampInvalid {
252                    got: prev_timestamp,
253                    parent: prev_timestamp,
254                })
255            })?;
256            overrides.time = Some(t);
257            t
258        };
259        prev_timestamp = block_time;
260
261        out.push(block);
262    }
263
264    Ok(out)
265}
266
267/// Applies precompile move overrides from state overrides to the EVM's precompiles map.
268///
269/// This function processes `movePrecompileToAddress` entries from the state overrides and
270/// moves precompiles from their original addresses to new addresses. The original address
271/// is cleared (precompile removed) and the precompile is installed at the destination address.
272pub fn apply_precompile_overrides(
273    state_overrides: &StateOverride,
274    precompiles: &mut PrecompilesMap,
275) -> Result<(), EthSimulateError> {
276    let moves: Vec<_> = state_overrides
277        .iter()
278        .filter_map(|(source, account_override)| {
279            account_override.move_precompile_to.map(|dest| (*source, dest))
280        })
281        .collect();
282
283    for (source, dest) in &moves {
284        if source == dest {
285            if precompiles.get(source).is_none() {
286                return Err(EthSimulateError::NotAPrecompile(*source))
287            }
288            return Err(EthSimulateError::MovePrecompileToSelf(*source))
289        }
290    }
291
292    precompiles.move_precompiles(moves).map_err(
293        |alloy_evm::precompiles::MovePrecompileError::NotAPrecompile(addr)| {
294            EthSimulateError::NotAPrecompile(addr)
295        },
296    )?;
297
298    Ok(())
299}
300
301/// Converts all [`TransactionRequest`]s into [`Recovered`] transactions and applies them to the
302/// given [`BlockExecutor`].
303///
304/// Returns all executed transactions and the result of the execution.
305///
306/// For each call without an explicit `gas` field, the remaining block gas is used as the default.
307/// The RPC gas cap is tracked as a request-wide remaining budget and caps each call before
308/// execution. This matches the spec rule `"gasLimit: blockGasLimit - soFarUsedGasInBlock"` and
309/// geth's per-call `sanitizeCall` behavior.
310///
311/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
312#[expect(clippy::type_complexity)]
313pub fn execute_transactions<S, T>(
314    mut builder: S,
315    state_provider: impl StateProvider,
316    calls: Vec<RpcTxReq<T::Network>>,
317    remaining_call_gas_limit: &mut Option<u64>,
318    chain_id: u64,
319    compute_state_root: bool,
320    converter: &T,
321) -> Result<
322    (
323        BlockBuilderOutcome<S::Primitives>,
324        Vec<ExecutionResult<<<S::Executor as BlockExecutor>::Evm as Evm>::HaltReason>>,
325    ),
326    EthApiError,
327>
328where
329    S: BlockBuilder<
330        Executor: BlockExecutor<
331            Evm: Evm<DB: Database<Error: Into<EthApiError>>, Spec: Into<SpecId>>,
332        >,
333    >,
334    T: RpcConvert<Primitives = S::Primitives>,
335{
336    builder.apply_pre_execution_changes()?;
337
338    let mut results = Vec::with_capacity(calls.len());
339    let mut cumulative_tx_gas_used: u64 = 0;
340    let mut block_regular_gas_used: u64 = 0;
341    let mut block_state_gas_used: u64 = 0;
342    let block_gas_limit = builder.evm().block().gas_limit();
343    let is_amsterdam = builder.evm().cfg_env().enable_amsterdam_eip8037;
344    let tx_gas_limit_cap = builder.evm().cfg_env().tx_gas_limit_cap();
345    for mut call in calls {
346        let block_gas_remaining = if is_amsterdam {
347            let regular_available_gas = block_gas_limit.saturating_sub(block_regular_gas_used);
348            let state_available_gas = block_gas_limit.saturating_sub(block_state_gas_used);
349            // Gas above the tx gas cap goes to the state gas reservoir and only needs state gas.
350            if regular_available_gas >= tx_gas_limit_cap {
351                state_available_gas
352            } else {
353                regular_available_gas.min(state_available_gas)
354            }
355        } else {
356            block_gas_limit.saturating_sub(cumulative_tx_gas_used)
357        };
358        let mut default_gas_limit = block_gas_remaining;
359
360        if let Some(gas_limit) = call.as_ref().gas_limit() &&
361            gas_limit > block_gas_remaining
362        {
363            return Err(EthApiError::other(EthSimulateError::BlockGasLimitExceeded))
364        }
365
366        if let Some(remaining_call_gas_limit) = *remaining_call_gas_limit {
367            if let Some(gas_limit) = call.as_ref().gas_limit() {
368                if gas_limit > remaining_call_gas_limit {
369                    call.as_mut().set_gas_limit(remaining_call_gas_limit);
370                }
371            } else {
372                default_gas_limit = default_gas_limit.min(remaining_call_gas_limit);
373            }
374        }
375
376        // Resolve transaction, populate missing fields and enforce calls
377        // correctness.
378        let tx = resolve_transaction(
379            call,
380            default_gas_limit,
381            builder.evm().block().basefee(),
382            chain_id,
383            builder.evm().cfg_env().disable_nonce_check,
384            builder.evm_mut().db_mut(),
385            converter,
386        )?;
387        // Create transaction with an empty envelope.
388        // The effect for a layer-2 execution client is that it does not charge L1 cost.
389        let tx = WithEncoded::new(Default::default(), tx);
390
391        let mut tx_regular_gas_used = 0;
392        let gas_output = builder.execute_transaction_with_result_closure(tx, |result| {
393            tx_regular_gas_used = result.result().result.gas().block_regular_gas_used();
394            results.push(result.result().result.clone())
395        })?;
396
397        let gas_used = gas_output.tx_gas_used();
398        if let Some(remaining_call_gas_limit) = remaining_call_gas_limit.as_mut() {
399            if gas_used > *remaining_call_gas_limit {
400                return Err(EthApiError::other(EthSimulateError::GasLimitReached))
401            }
402            *remaining_call_gas_limit -= gas_used;
403        }
404
405        cumulative_tx_gas_used = cumulative_tx_gas_used.saturating_add(gas_used);
406        block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas_used);
407        block_state_gas_used = block_state_gas_used.saturating_add(gas_output.state_gas_used());
408    }
409
410    let result = if compute_state_root {
411        builder.finish(state_provider, None)?
412    } else {
413        builder.finish(NoopProvider::default(), None)?
414    };
415
416    Ok((result, results))
417}
418
419/// Goes over the list of [`TransactionRequest`]s and populates missing fields trying to resolve
420/// them into primitive transactions.
421///
422/// This will set the defaults as defined in <https://github.com/ethereum/execution-apis/blob/e56d3208789259d0b09fa68e9d8594aa4d73c725/docs/ethsimulatev1-notes.md#default-values-for-transactions>
423///
424/// [`TransactionRequest`]: alloy_rpc_types_eth::TransactionRequest
425pub fn resolve_transaction<DB: Database, Tx, T>(
426    mut tx: RpcTxReq<T::Network>,
427    default_gas_limit: u64,
428    block_base_fee_per_gas: u64,
429    chain_id: u64,
430    disable_nonce_check: bool,
431    db: &mut DB,
432    converter: &T,
433) -> Result<Recovered<Tx>, EthApiError>
434where
435    DB::Error: Into<EthApiError>,
436    T: RpcConvert<Primitives: NodePrimitives<SignedTx = Tx>>,
437{
438    // If we're missing any fields we try to fill nonce, gas and
439    // gas price.
440    let tx_type = tx.as_ref().output_tx_type();
441
442    let from = if let Some(from) = tx.as_ref().from() {
443        from
444    } else {
445        tx.as_mut().set_from(Address::ZERO);
446        Address::ZERO
447    };
448
449    let state_nonce = db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default();
450    if tx.as_ref().nonce().is_none() {
451        tx.as_mut().set_nonce(state_nonce);
452    }
453    // EIP-2681 applies in both modes. Check it here because revm checks the fee cap first.
454    if state_nonce == u64::MAX && (disable_nonce_check || tx.as_ref().nonce() == Some(u64::MAX)) {
455        return Err(EthApiError::other(EthSimulateError::NonceMaxValue))
456    }
457    // revm rejects a tx nonce of u64::MAX even with the nonce check disabled. The state nonce is
458    // below the limit here, so only the tx object changes.
459    if disable_nonce_check && tx.as_ref().nonce() == Some(u64::MAX) {
460        tx.as_mut().set_nonce(0);
461    }
462
463    if tx.as_ref().gas_limit().is_none() {
464        tx.as_mut().set_gas_limit(default_gas_limit);
465    }
466
467    if tx.as_ref().chain_id().is_none() {
468        tx.as_mut().set_chain_id(chain_id);
469    }
470
471    if tx.as_ref().kind().is_none() {
472        tx.as_mut().set_kind(TxKind::Create);
473    }
474
475    // if we can't build the _entire_ transaction yet, fill the fee fields.
476    //
477    // Per the eth_simulateV1 spec, unspecified fee fields default to 0 (not the block base fee),
478    // matching geth's `CallDefaults` behavior. This lets simulation behave like a free-gas
479    // `eth_call` when validation is off, and surfaces "max fee per gas less than block base fee"
480    // errors when validation is on with a real base fee.
481    let _ = block_base_fee_per_gas;
482    if tx.as_ref().output_tx_type_checked().is_none() {
483        if tx_type.is_legacy() || tx_type.is_eip2930() {
484            if tx.as_ref().gas_price().is_none() {
485                tx.as_mut().set_gas_price(0);
486            }
487        } else {
488            if tx.as_ref().max_fee_per_gas().is_none() {
489                tx.as_mut().set_max_fee_per_gas(0);
490            }
491            if tx.as_ref().max_priority_fee_per_gas().is_none() {
492                tx.as_mut().set_max_priority_fee_per_gas(0);
493            }
494        }
495    }
496
497    let tx =
498        converter.build_simulate_v1_transaction(tx).map_err(|e| EthApiError::other(e.into()))?;
499
500    Ok(Recovered::new_unchecked(tx, from))
501}
502
503/// Handles outputs of the calls execution and builds a [`SimulatedBlock`].
504pub fn build_simulated_block<Err, T>(
505    block: RecoveredBlock<BlockTy<T::Primitives>>,
506    results: Vec<ExecutionResult<HaltReasonFor<T::Evm>>>,
507    txs_kind: BlockTransactionsKind,
508    converter: &T,
509) -> Result<SimulatedBlock<RpcBlock<T::Network>>, Err>
510where
511    Err: std::error::Error
512        + FromEthApiError
513        + FromEvmError<T::Evm>
514        + From<T::Error>
515        + Into<jsonrpsee_types::ErrorObject<'static>>,
516    T: RpcConvert,
517{
518    let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
519
520    let mut log_index = 0;
521    for (index, (result, tx)) in results.into_iter().zip(block.body().transactions()).enumerate() {
522        let call = match result {
523            ExecutionResult::Halt { reason, gas, .. } => {
524                let error = Err::from_evm_halt(reason, tx.gas_limit());
525                SimCallResult {
526                    return_data: Bytes::new(),
527                    error: Some(SimulateError {
528                        message: error.to_string(),
529                        code: SIMULATE_VM_ERROR_CODE,
530                        ..SimulateError::invalid_params()
531                    }),
532                    gas_used: gas.tx_gas_used(),
533                    max_used_gas: Some(gas.total_gas_spent().max(gas.floor_gas())),
534                    logs: Vec::new(),
535                    status: false,
536                }
537            }
538            ExecutionResult::Revert { output, gas, .. } => {
539                let error = Err::from_revert(output.clone());
540                SimCallResult {
541                    return_data: Bytes::new(),
542                    error: Some(SimulateError {
543                        message: error.to_string(),
544                        code: SIMULATE_REVERT_CODE,
545                        data: Some(output),
546                    }),
547                    gas_used: gas.tx_gas_used(),
548                    max_used_gas: Some(gas.total_gas_spent().max(gas.floor_gas())),
549                    status: false,
550                    logs: Vec::new(),
551                }
552            }
553            ExecutionResult::Success { output, gas, logs, .. } => SimCallResult {
554                return_data: output.into_data(),
555                error: None,
556                gas_used: gas.tx_gas_used(),
557                max_used_gas: Some(gas.total_gas_spent().max(gas.floor_gas())),
558                logs: logs
559                    .into_iter()
560                    .map(|log| {
561                        log_index += 1;
562                        alloy_rpc_types_eth::Log {
563                            inner: log,
564                            log_index: Some(log_index - 1),
565                            transaction_index: Some(index as u64),
566                            transaction_hash: Some(*tx.tx_hash()),
567                            block_hash: Some(block.hash()),
568                            block_number: Some(block.header().number()),
569                            block_timestamp: Some(block.header().timestamp()),
570                            ..Default::default()
571                        }
572                    })
573                    .collect(),
574                status: true,
575            },
576        };
577
578        calls.push(call);
579    }
580
581    let block = block.into_rpc_block(
582        txs_kind,
583        |tx, tx_info| converter.fill(tx, tx_info),
584        |header, block_size| converter.convert_header(header, Some(block_size)),
585    )?;
586    Ok(SimulatedBlock { inner: block, calls })
587}
588
589#[cfg(test)]
590mod tests {
591    use super::{
592        apply_precompile_overrides, sanitize_chain, EthSimulateError, INTERNAL_ERROR_CODE,
593    };
594    use crate::{error::ToRpcError, EthApiError};
595    use alloy_chains::Chain;
596    use alloy_consensus::Header;
597    use alloy_evm::precompiles::PrecompilesMap;
598    use alloy_primitives::{address, Address, U256};
599    use alloy_rpc_types_eth::{
600        simulate::SimBlock,
601        state::{AccountOverride, StateOverride},
602        BlockOverrides, TransactionRequest,
603    };
604    use reth_primitives_traits::SealedHeader;
605    use revm::precompile::Precompiles;
606
607    #[test]
608    fn nonce_max_value_error_uses_internal_error_code() {
609        let err = EthSimulateError::NonceMaxValue.to_rpc_error();
610
611        assert_eq!(err.code(), INTERNAL_ERROR_CODE);
612        assert_eq!(err.message(), "nonce has max value");
613    }
614
615    #[test]
616    fn block_not_found_error_uses_simulate_code() {
617        let err = EthSimulateError::BlockNotFound { block: 100000.into() }.to_rpc_error();
618
619        assert_eq!(err.code(), -32000);
620        assert_eq!(err.message(), "block not found: 0x186a0");
621    }
622
623    fn parent_at(number: u64, timestamp: u64) -> SealedHeader<Header> {
624        SealedHeader::seal_slow(Header { number, timestamp, ..Default::default() })
625    }
626
627    fn block_with_number(number: u64) -> SimBlock<TransactionRequest> {
628        SimBlock {
629            block_overrides: Some(BlockOverrides {
630                number: Some(U256::from(number)),
631                ..Default::default()
632            }),
633            ..Default::default()
634        }
635    }
636
637    #[test]
638    fn precompile_self_move_requires_existing_precompile() {
639        let address = address!("c100000000000000000000000000000000000000");
640        let mut state_overrides = StateOverride::default();
641        state_overrides.insert(
642            address,
643            AccountOverride { move_precompile_to: Some(address), ..Default::default() },
644        );
645        let mut precompiles = PrecompilesMap::from_static(Precompiles::prague());
646
647        let err = apply_precompile_overrides(&state_overrides, &mut precompiles).unwrap_err();
648
649        assert!(matches!(err, EthSimulateError::NotAPrecompile(addr) if addr == address));
650    }
651
652    #[test]
653    fn precompile_self_move_errors_for_existing_precompile() {
654        let address = Address::with_last_byte(1);
655        let mut state_overrides = StateOverride::default();
656        state_overrides.insert(
657            address,
658            AccountOverride { move_precompile_to: Some(address), ..Default::default() },
659        );
660        let mut precompiles = PrecompilesMap::from_static(Precompiles::prague());
661
662        let err = apply_precompile_overrides(&state_overrides, &mut precompiles).unwrap_err();
663
664        assert!(matches!(err, EthSimulateError::MovePrecompileToSelf(addr) if addr == address));
665    }
666
667    #[test]
668    fn moved_precompile_is_callable() {
669        let source = Address::with_last_byte(1);
670        let dest = address!("0000000000000000000000000000000000123456");
671        let mut state_overrides = StateOverride::default();
672        state_overrides.insert(
673            source,
674            AccountOverride { move_precompile_to: Some(dest), ..Default::default() },
675        );
676        let mut precompiles = PrecompilesMap::from_static(Precompiles::prague());
677
678        apply_precompile_overrides(&state_overrides, &mut precompiles).unwrap();
679
680        assert!(precompiles.get(&source).is_none());
681        assert!(precompiles.get(&dest).is_some());
682    }
683
684    #[test]
685    fn sanitize_chain_fills_gaps_with_empty_blocks() {
686        // parent at block 5; user requests one block at 8 — sanitize should insert fillers at 6
687        // and 7 before the requested block.
688        let parent = parent_at(5, 100);
689        let blocks = vec![block_with_number(8)];
690
691        let out = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap();
692        assert_eq!(out.len(), 3);
693
694        let numbers: Vec<u64> = out
695            .iter()
696            .map(|b| b.block_overrides.as_ref().unwrap().number.unwrap().try_into().unwrap())
697            .collect();
698        assert_eq!(numbers, vec![6, 7, 8]);
699
700        assert!(out[0].calls.is_empty());
701        assert!(out[1].calls.is_empty());
702
703        // Mainnet hint is 12s, so timestamps should auto-increment from 100 by 12.
704        let times: Vec<u64> =
705            out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
706        assert_eq!(times, vec![112, 124, 136]);
707    }
708
709    #[test]
710    fn sanitize_chain_defaults_missing_number_and_time() {
711        let parent = parent_at(10, 1000);
712        let blocks: Vec<SimBlock<TransactionRequest>> =
713            vec![SimBlock::default(), SimBlock::default()];
714
715        let out = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap();
716        assert_eq!(out.len(), 2);
717
718        let overrides = out[0].block_overrides.as_ref().unwrap();
719        assert_eq!(overrides.number.unwrap(), U256::from(11));
720        assert_eq!(overrides.time, Some(1012));
721
722        let overrides = out[1].block_overrides.as_ref().unwrap();
723        assert_eq!(overrides.number.unwrap(), U256::from(12));
724        assert_eq!(overrides.time, Some(1024));
725    }
726
727    #[test]
728    fn sanitize_chain_uses_chain_blocktime_hint() {
729        // Optimism has a 2s blocktime hint; filler/auto timestamps should reflect that.
730        let parent = parent_at(0, 0);
731        let blocks = vec![block_with_number(3)];
732
733        let out = sanitize_chain(blocks, &parent, Chain::optimism_mainnet().id(), 256).unwrap();
734        let times: Vec<u64> =
735            out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
736        assert_eq!(times, vec![2, 4, 6]);
737    }
738
739    #[test]
740    fn sanitize_chain_rounds_subsecond_blocktime_hint_up() {
741        // Arbitrum has a 260ms blocktime hint. Simulated timestamps are second-granular, so this
742        // rounds up to a 1s increment instead of falling back to the default.
743        let parent = parent_at(0, 0);
744        let blocks = vec![block_with_number(3)];
745
746        let out = sanitize_chain(blocks, &parent, Chain::arbitrum_mainnet().id(), 256).unwrap();
747        let times: Vec<u64> =
748            out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
749        assert_eq!(times, vec![1, 2, 3]);
750    }
751
752    #[test]
753    fn sanitize_chain_falls_back_when_chain_has_no_hint() {
754        // An unknown chain id has no blocktime hint — fall back to the 12s default.
755        let parent = parent_at(0, 0);
756        let blocks = vec![block_with_number(2)];
757
758        let out = sanitize_chain(blocks, &parent, Chain::from_id(123_456_789).id(), 256).unwrap();
759        let times: Vec<u64> =
760            out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
761        assert_eq!(times, vec![12, 24]);
762    }
763
764    #[test]
765    fn sanitize_chain_rejects_non_increasing_number() {
766        let parent = parent_at(10, 100);
767        let err = sanitize_chain(vec![block_with_number(10)], &parent, Chain::mainnet().id(), 256)
768            .unwrap_err();
769        assert!(matches!(err, EthApiError::Other(_)));
770    }
771
772    #[test]
773    fn sanitize_chain_rejects_timestamp_overflow() {
774        // A block may set any timestamp above its parent's, including `u64::MAX`. The following
775        // block then defaults to `prev + increment`, which must not wrap.
776        let parent = parent_at(0, 0);
777        let blocks: Vec<SimBlock<TransactionRequest>> = vec![
778            SimBlock {
779                block_overrides: Some(BlockOverrides {
780                    time: Some(u64::MAX),
781                    ..Default::default()
782                }),
783                ..Default::default()
784            },
785            SimBlock::default(),
786        ];
787
788        let err = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap_err();
789        assert!(matches!(err, EthApiError::Other(_)));
790    }
791
792    #[test]
793    fn sanitize_chain_rejects_filler_timestamp_overflow() {
794        // Same, but the wrap would happen while generating filler blocks for a number gap.
795        let parent = parent_at(0, 0);
796        let blocks = vec![
797            SimBlock {
798                block_overrides: Some(BlockOverrides {
799                    number: Some(U256::ONE),
800                    time: Some(u64::MAX),
801                    ..Default::default()
802                }),
803                ..Default::default()
804            },
805            block_with_number(4),
806        ];
807
808        let err = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap_err();
809        assert!(matches!(err, EthApiError::Other(_)));
810    }
811
812    #[test]
813    fn sanitize_chain_enforces_max_blocks() {
814        let parent = parent_at(0, 0);
815        let err = sanitize_chain(vec![block_with_number(257)], &parent, Chain::mainnet().id(), 256)
816            .unwrap_err();
817        assert!(matches!(err, EthApiError::Other(_)));
818    }
819}