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