1use 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
35const SIMULATE_FALLBACK_TIMESTAMP_INCREMENT: u64 = 12;
38
39pub const SIMULATE_REVERT_CODE: i32 = 3;
45
46pub const SIMULATE_VM_ERROR_CODE: i32 = -32015;
50
51#[derive(Debug, thiserror::Error)]
53pub enum EthSimulateError {
54 #[error("Block gas limit exceeded by the block's transactions")]
56 BlockGasLimitExceeded,
57 #[error("too many blocks")]
59 TooManyBlocks,
60 #[error("Client adjustable limit reached")]
62 GasLimitReached,
63 #[error("block not found: {block}")]
65 BlockNotFound {
66 block: BlockId,
68 },
69 #[error("block numbers must be in order: {got} <= {parent}")]
71 BlockNumberInvalid {
72 got: u64,
74 parent: u64,
76 },
77 #[error("block timestamps must be in order: {got} <= {parent}")]
79 BlockTimestampInvalid {
80 got: u64,
82 parent: u64,
84 },
85 #[error("nonce too low: next nonce {state}, tx nonce {tx}")]
87 NonceTooLow {
88 tx: u64,
90 state: u64,
92 },
93 #[error("nonce too high")]
95 NonceTooHigh,
96 #[error("nonce has max value")]
98 NonceMaxValue,
99 #[error("max fee per gas less than block base fee")]
101 BaseFeePerGasTooLow,
102 #[error("intrinsic gas too low")]
104 IntrinsicGasTooLow,
105 #[error("insufficient funds for gas * price + value: have {balance} want {cost}")]
107 InsufficientFunds {
108 cost: U256,
110 balance: U256,
112 },
113 #[error("sender is not an EOA")]
115 SenderNotEOA,
116 #[error("max initcode size exceeded")]
118 MaxInitCodeSizeExceeded,
119 #[error("account {0} is not a precompile")]
121 NotAPrecompile(Address),
122 #[error("cannot move precompile {0} to itself")]
124 MovePrecompileToSelf(Address),
125}
126
127impl EthSimulateError {
128 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
155pub 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 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 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 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
267pub 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#[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 block_gas_limit
348 .saturating_sub(block_regular_gas_used)
349 .min(block_gas_limit.saturating_sub(block_state_gas_used))
350 } else {
351 block_gas_limit.saturating_sub(cumulative_tx_gas_used)
352 };
353 let mut default_gas_limit = block_gas_remaining;
354
355 if let Some(gas_limit) = call.as_ref().gas_limit() {
356 let exceeds_gas_limit = if is_amsterdam {
357 let regular_available_gas = block_gas_limit.saturating_sub(block_regular_gas_used);
358 let state_available_gas = block_gas_limit.saturating_sub(block_state_gas_used);
359 let regular_tx_gas_limit = gas_limit.min(tx_gas_limit_cap);
360
361 regular_tx_gas_limit > regular_available_gas || gas_limit > state_available_gas
362 } else {
363 gas_limit > block_gas_remaining
364 };
365
366 if exceeds_gas_limit {
367 return Err(EthApiError::other(EthSimulateError::BlockGasLimitExceeded))
368 }
369 }
370
371 if let Some(remaining_call_gas_limit) = *remaining_call_gas_limit {
372 if let Some(gas_limit) = call.as_ref().gas_limit() {
373 if gas_limit > remaining_call_gas_limit {
374 call.as_mut().set_gas_limit(remaining_call_gas_limit);
375 }
376 } else {
377 default_gas_limit = default_gas_limit.min(remaining_call_gas_limit);
378 }
379 }
380
381 let tx = resolve_transaction(
384 call,
385 default_gas_limit,
386 builder.evm().block().basefee(),
387 chain_id,
388 builder.evm().cfg_env().disable_nonce_check,
389 builder.evm_mut().db_mut(),
390 converter,
391 )?;
392 let tx = WithEncoded::new(Default::default(), tx);
395
396 let mut tx_regular_gas_used = 0;
397 let gas_output = builder.execute_transaction_with_result_closure(tx, |result| {
398 tx_regular_gas_used = result.result().result.gas().block_regular_gas_used();
399 results.push(result.result().result.clone())
400 })?;
401
402 let gas_used = gas_output.tx_gas_used();
403 if let Some(remaining_call_gas_limit) = remaining_call_gas_limit.as_mut() {
404 if gas_used > *remaining_call_gas_limit {
405 return Err(EthApiError::other(EthSimulateError::GasLimitReached))
406 }
407 *remaining_call_gas_limit -= gas_used;
408 }
409
410 cumulative_tx_gas_used = cumulative_tx_gas_used.saturating_add(gas_used);
411 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas_used);
412 block_state_gas_used = block_state_gas_used.saturating_add(gas_output.state_gas_used());
413 }
414
415 let result = if compute_state_root {
416 builder.finish(state_provider, None)?
417 } else {
418 builder.finish(NoopProvider::default(), None)?
419 };
420
421 Ok((result, results))
422}
423
424pub fn resolve_transaction<DB: Database, Tx, T>(
431 mut tx: RpcTxReq<T::Network>,
432 default_gas_limit: u64,
433 block_base_fee_per_gas: u64,
434 chain_id: u64,
435 disable_nonce_check: bool,
436 db: &mut DB,
437 converter: &T,
438) -> Result<Recovered<Tx>, EthApiError>
439where
440 DB::Error: Into<EthApiError>,
441 T: RpcConvert<Primitives: NodePrimitives<SignedTx = Tx>>,
442{
443 let tx_type = tx.as_ref().output_tx_type();
446
447 let from = if let Some(from) = tx.as_ref().from() {
448 from
449 } else {
450 tx.as_mut().set_from(Address::ZERO);
451 Address::ZERO
452 };
453
454 if tx.as_ref().nonce().is_none() {
455 tx.as_mut().set_nonce(
456 db.basic(from).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default(),
457 );
458 }
459 if disable_nonce_check && tx.as_ref().nonce() == Some(u64::MAX) {
461 tx.as_mut().set_nonce(0);
462 }
463
464 if tx.as_ref().gas_limit().is_none() {
465 tx.as_mut().set_gas_limit(default_gas_limit);
466 }
467
468 if tx.as_ref().chain_id().is_none() {
469 tx.as_mut().set_chain_id(chain_id);
470 }
471
472 if tx.as_ref().kind().is_none() {
473 tx.as_mut().set_kind(TxKind::Create);
474 }
475
476 let _ = block_base_fee_per_gas;
483 if tx.as_ref().output_tx_type_checked().is_none() {
484 if tx_type.is_legacy() || tx_type.is_eip2930() {
485 if tx.as_ref().gas_price().is_none() {
486 tx.as_mut().set_gas_price(0);
487 }
488 } else {
489 if tx.as_ref().max_fee_per_gas().is_none() {
490 tx.as_mut().set_max_fee_per_gas(0);
491 }
492 if tx.as_ref().max_priority_fee_per_gas().is_none() {
493 tx.as_mut().set_max_priority_fee_per_gas(0);
494 }
495 }
496 }
497
498 let tx =
499 converter.build_simulate_v1_transaction(tx).map_err(|e| EthApiError::other(e.into()))?;
500
501 Ok(Recovered::new_unchecked(tx, from))
502}
503
504pub fn build_simulated_block<Err, T>(
506 block: RecoveredBlock<BlockTy<T::Primitives>>,
507 results: Vec<ExecutionResult<HaltReasonFor<T::Evm>>>,
508 txs_kind: BlockTransactionsKind,
509 converter: &T,
510) -> Result<SimulatedBlock<RpcBlock<T::Network>>, Err>
511where
512 Err: std::error::Error
513 + FromEthApiError
514 + FromEvmError<T::Evm>
515 + From<T::Error>
516 + Into<jsonrpsee_types::ErrorObject<'static>>,
517 T: RpcConvert,
518{
519 let mut calls: Vec<SimCallResult> = Vec::with_capacity(results.len());
520
521 let mut log_index = 0;
522 for (index, (result, tx)) in results.into_iter().zip(block.body().transactions()).enumerate() {
523 let call = match result {
524 ExecutionResult::Halt { reason, gas, .. } => {
525 let error = Err::from_evm_halt(reason, tx.gas_limit());
526 SimCallResult {
527 return_data: Bytes::new(),
528 error: Some(SimulateError {
529 message: error.to_string(),
530 code: SIMULATE_VM_ERROR_CODE,
531 ..SimulateError::invalid_params()
532 }),
533 gas_used: gas.tx_gas_used(),
534 max_used_gas: Some(gas.total_gas_spent().max(gas.floor_gas())),
535 logs: Vec::new(),
536 status: false,
537 }
538 }
539 ExecutionResult::Revert { output, gas, .. } => {
540 let error = Err::from_revert(output.clone());
541 SimCallResult {
542 return_data: Bytes::new(),
543 error: Some(SimulateError {
544 message: error.to_string(),
545 code: SIMULATE_REVERT_CODE,
546 data: Some(output),
547 }),
548 gas_used: gas.tx_gas_used(),
549 max_used_gas: Some(gas.total_gas_spent().max(gas.floor_gas())),
550 status: false,
551 logs: Vec::new(),
552 }
553 }
554 ExecutionResult::Success { output, gas, logs, .. } => SimCallResult {
555 return_data: output.into_data(),
556 error: None,
557 gas_used: gas.tx_gas_used(),
558 max_used_gas: Some(gas.total_gas_spent().max(gas.floor_gas())),
559 logs: logs
560 .into_iter()
561 .map(|log| {
562 log_index += 1;
563 alloy_rpc_types_eth::Log {
564 inner: log,
565 log_index: Some(log_index - 1),
566 transaction_index: Some(index as u64),
567 transaction_hash: Some(*tx.tx_hash()),
568 block_hash: Some(block.hash()),
569 block_number: Some(block.header().number()),
570 block_timestamp: Some(block.header().timestamp()),
571 ..Default::default()
572 }
573 })
574 .collect(),
575 status: true,
576 },
577 };
578
579 calls.push(call);
580 }
581
582 let block = block.into_rpc_block(
583 txs_kind,
584 |tx, tx_info| converter.fill(tx, tx_info),
585 |header, block_size| converter.convert_header(header, Some(block_size)),
586 )?;
587 Ok(SimulatedBlock { inner: block, calls })
588}
589
590#[cfg(test)]
591mod tests {
592 use super::{
593 apply_precompile_overrides, sanitize_chain, EthSimulateError, INTERNAL_ERROR_CODE,
594 };
595 use crate::{error::ToRpcError, EthApiError};
596 use alloy_chains::Chain;
597 use alloy_consensus::Header;
598 use alloy_evm::precompiles::PrecompilesMap;
599 use alloy_primitives::{address, Address, U256};
600 use alloy_rpc_types_eth::{
601 simulate::SimBlock,
602 state::{AccountOverride, StateOverride},
603 BlockOverrides, TransactionRequest,
604 };
605 use reth_primitives_traits::SealedHeader;
606 use revm::precompile::Precompiles;
607
608 #[test]
609 fn nonce_max_value_error_uses_internal_error_code() {
610 let err = EthSimulateError::NonceMaxValue.to_rpc_error();
611
612 assert_eq!(err.code(), INTERNAL_ERROR_CODE);
613 assert_eq!(err.message(), "nonce has max value");
614 }
615
616 #[test]
617 fn block_not_found_error_uses_simulate_code() {
618 let err = EthSimulateError::BlockNotFound { block: 100000.into() }.to_rpc_error();
619
620 assert_eq!(err.code(), -32000);
621 assert_eq!(err.message(), "block not found: 0x186a0");
622 }
623
624 fn parent_at(number: u64, timestamp: u64) -> SealedHeader<Header> {
625 SealedHeader::seal_slow(Header { number, timestamp, ..Default::default() })
626 }
627
628 fn block_with_number(number: u64) -> SimBlock<TransactionRequest> {
629 SimBlock {
630 block_overrides: Some(BlockOverrides {
631 number: Some(U256::from(number)),
632 ..Default::default()
633 }),
634 ..Default::default()
635 }
636 }
637
638 #[test]
639 fn precompile_self_move_requires_existing_precompile() {
640 let address = address!("c100000000000000000000000000000000000000");
641 let mut state_overrides = StateOverride::default();
642 state_overrides.insert(
643 address,
644 AccountOverride { move_precompile_to: Some(address), ..Default::default() },
645 );
646 let mut precompiles = PrecompilesMap::from_static(Precompiles::prague());
647
648 let err = apply_precompile_overrides(&state_overrides, &mut precompiles).unwrap_err();
649
650 assert!(matches!(err, EthSimulateError::NotAPrecompile(addr) if addr == address));
651 }
652
653 #[test]
654 fn precompile_self_move_errors_for_existing_precompile() {
655 let address = Address::with_last_byte(1);
656 let mut state_overrides = StateOverride::default();
657 state_overrides.insert(
658 address,
659 AccountOverride { move_precompile_to: Some(address), ..Default::default() },
660 );
661 let mut precompiles = PrecompilesMap::from_static(Precompiles::prague());
662
663 let err = apply_precompile_overrides(&state_overrides, &mut precompiles).unwrap_err();
664
665 assert!(matches!(err, EthSimulateError::MovePrecompileToSelf(addr) if addr == address));
666 }
667
668 #[test]
669 fn moved_precompile_is_callable() {
670 let source = Address::with_last_byte(1);
671 let dest = address!("0000000000000000000000000000000000123456");
672 let mut state_overrides = StateOverride::default();
673 state_overrides.insert(
674 source,
675 AccountOverride { move_precompile_to: Some(dest), ..Default::default() },
676 );
677 let mut precompiles = PrecompilesMap::from_static(Precompiles::prague());
678
679 apply_precompile_overrides(&state_overrides, &mut precompiles).unwrap();
680
681 assert!(precompiles.get(&source).is_none());
682 assert!(precompiles.get(&dest).is_some());
683 }
684
685 #[test]
686 fn sanitize_chain_fills_gaps_with_empty_blocks() {
687 let parent = parent_at(5, 100);
690 let blocks = vec![block_with_number(8)];
691
692 let out = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap();
693 assert_eq!(out.len(), 3);
694
695 let numbers: Vec<u64> = out
696 .iter()
697 .map(|b| b.block_overrides.as_ref().unwrap().number.unwrap().try_into().unwrap())
698 .collect();
699 assert_eq!(numbers, vec![6, 7, 8]);
700
701 assert!(out[0].calls.is_empty());
702 assert!(out[1].calls.is_empty());
703
704 let times: Vec<u64> =
706 out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
707 assert_eq!(times, vec![112, 124, 136]);
708 }
709
710 #[test]
711 fn sanitize_chain_defaults_missing_number_and_time() {
712 let parent = parent_at(10, 1000);
713 let blocks: Vec<SimBlock<TransactionRequest>> =
714 vec![SimBlock::default(), SimBlock::default()];
715
716 let out = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap();
717 assert_eq!(out.len(), 2);
718
719 let overrides = out[0].block_overrides.as_ref().unwrap();
720 assert_eq!(overrides.number.unwrap(), U256::from(11));
721 assert_eq!(overrides.time, Some(1012));
722
723 let overrides = out[1].block_overrides.as_ref().unwrap();
724 assert_eq!(overrides.number.unwrap(), U256::from(12));
725 assert_eq!(overrides.time, Some(1024));
726 }
727
728 #[test]
729 fn sanitize_chain_uses_chain_blocktime_hint() {
730 let parent = parent_at(0, 0);
732 let blocks = vec![block_with_number(3)];
733
734 let out = sanitize_chain(blocks, &parent, Chain::optimism_mainnet().id(), 256).unwrap();
735 let times: Vec<u64> =
736 out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
737 assert_eq!(times, vec![2, 4, 6]);
738 }
739
740 #[test]
741 fn sanitize_chain_rounds_subsecond_blocktime_hint_up() {
742 let parent = parent_at(0, 0);
745 let blocks = vec![block_with_number(3)];
746
747 let out = sanitize_chain(blocks, &parent, Chain::arbitrum_mainnet().id(), 256).unwrap();
748 let times: Vec<u64> =
749 out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
750 assert_eq!(times, vec![1, 2, 3]);
751 }
752
753 #[test]
754 fn sanitize_chain_falls_back_when_chain_has_no_hint() {
755 let parent = parent_at(0, 0);
757 let blocks = vec![block_with_number(2)];
758
759 let out = sanitize_chain(blocks, &parent, Chain::from_id(123_456_789).id(), 256).unwrap();
760 let times: Vec<u64> =
761 out.iter().map(|b| b.block_overrides.as_ref().unwrap().time.unwrap()).collect();
762 assert_eq!(times, vec![12, 24]);
763 }
764
765 #[test]
766 fn sanitize_chain_rejects_non_increasing_number() {
767 let parent = parent_at(10, 100);
768 let err = sanitize_chain(vec![block_with_number(10)], &parent, Chain::mainnet().id(), 256)
769 .unwrap_err();
770 assert!(matches!(err, EthApiError::Other(_)));
771 }
772
773 #[test]
774 fn sanitize_chain_rejects_timestamp_overflow() {
775 let parent = parent_at(0, 0);
778 let blocks: Vec<SimBlock<TransactionRequest>> = vec![
779 SimBlock {
780 block_overrides: Some(BlockOverrides {
781 time: Some(u64::MAX),
782 ..Default::default()
783 }),
784 ..Default::default()
785 },
786 SimBlock::default(),
787 ];
788
789 let err = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap_err();
790 assert!(matches!(err, EthApiError::Other(_)));
791 }
792
793 #[test]
794 fn sanitize_chain_rejects_filler_timestamp_overflow() {
795 let parent = parent_at(0, 0);
797 let blocks = vec![
798 SimBlock {
799 block_overrides: Some(BlockOverrides {
800 number: Some(U256::ONE),
801 time: Some(u64::MAX),
802 ..Default::default()
803 }),
804 ..Default::default()
805 },
806 block_with_number(4),
807 ];
808
809 let err = sanitize_chain(blocks, &parent, Chain::mainnet().id(), 256).unwrap_err();
810 assert!(matches!(err, EthApiError::Other(_)));
811 }
812
813 #[test]
814 fn sanitize_chain_enforces_max_blocks() {
815 let parent = parent_at(0, 0);
816 let err = sanitize_chain(vec![block_with_number(257)], &parent, Chain::mainnet().id(), 256)
817 .unwrap_err();
818 assert!(matches!(err, EthApiError::Other(_)));
819 }
820}