Skip to main content

revm_handler/
validation.rs

1use context_interface::{
2    result::{InvalidHeader, InvalidTransaction},
3    transaction::{Transaction, TransactionType},
4    Block, Cfg, ContextTr,
5};
6use core::cmp;
7use interpreter::{instructions::calculate_initial_tx_gas_for_tx, InitialAndFloorGas};
8use primitives::{eip4844, hardfork::SpecId, B256};
9
10/// Validates the execution environment including block and transaction parameters.
11pub fn validate_env<CTX: ContextTr, ERROR: From<InvalidHeader> + From<InvalidTransaction>>(
12    context: CTX,
13) -> Result<(), ERROR> {
14    let spec = context.cfg().spec().into();
15    // `prevrandao` is required for the merge
16    if spec.is_enabled_in(SpecId::MERGE) && context.block().prevrandao().is_none() {
17        return Err(InvalidHeader::PrevrandaoNotSet.into());
18    }
19    // `excess_blob_gas` is required for Cancun
20    if spec.is_enabled_in(SpecId::CANCUN) && context.block().blob_excess_gas_and_price().is_none() {
21        return Err(InvalidHeader::ExcessBlobGasNotSet.into());
22    }
23    validate_tx_env::<CTX>(context, spec).map_err(Into::into)
24}
25
26/// Validate legacy transaction gas price against basefee.
27#[inline]
28pub const fn validate_legacy_gas_price(
29    gas_price: u128,
30    base_fee: Option<u128>,
31) -> Result<(), InvalidTransaction> {
32    // Gas price must be at least the basefee.
33    if let Some(base_fee) = base_fee {
34        if gas_price < base_fee {
35            return Err(InvalidTransaction::GasPriceLessThanBasefee);
36        }
37    }
38    Ok(())
39}
40
41/// Validate transaction that has EIP-1559 priority fee
42pub fn validate_priority_fee_tx(
43    max_fee: u128,
44    max_priority_fee: u128,
45    base_fee: Option<u128>,
46    disable_priority_fee_check: bool,
47) -> Result<(), InvalidTransaction> {
48    if !disable_priority_fee_check && max_priority_fee > max_fee {
49        // Or gas_max_fee for eip1559
50        return Err(InvalidTransaction::PriorityFeeGreaterThanMaxFee);
51    }
52
53    // Check minimal cost against basefee
54    if let Some(base_fee) = base_fee {
55        let effective_gas_price = cmp::min(max_fee, base_fee.saturating_add(max_priority_fee));
56        if effective_gas_price < base_fee {
57            return Err(InvalidTransaction::GasPriceLessThanBasefee);
58        }
59    }
60
61    Ok(())
62}
63
64/// Validate priority fee for transactions that support EIP-1559 (Eip1559, Eip4844, Eip7702).
65#[inline]
66fn validate_priority_fee_for_tx<TX: Transaction>(
67    tx: TX,
68    base_fee: Option<u128>,
69    disable_priority_fee_check: bool,
70) -> Result<(), InvalidTransaction> {
71    validate_priority_fee_tx(
72        tx.max_fee_per_gas(),
73        tx.max_priority_fee_per_gas().unwrap_or_default(),
74        base_fee,
75        disable_priority_fee_check,
76    )
77}
78
79/// Validate EIP-4844 transaction.
80pub fn validate_eip4844_tx(
81    blobs: &[B256],
82    max_blob_fee: u128,
83    block_blob_gas_price: u128,
84    max_blobs: Option<u64>,
85) -> Result<(), InvalidTransaction> {
86    // Ensure that the user was willing to at least pay the current blob gasprice
87    if block_blob_gas_price > max_blob_fee {
88        return Err(InvalidTransaction::BlobGasPriceGreaterThanMax {
89            block_blob_gas_price,
90            tx_max_fee_per_blob_gas: max_blob_fee,
91        });
92    }
93
94    // There must be at least one blob
95    if blobs.is_empty() {
96        return Err(InvalidTransaction::EmptyBlobs);
97    }
98
99    // All versioned blob hashes must start with VERSIONED_HASH_VERSION_KZG
100    for blob in blobs {
101        if blob[0] != eip4844::VERSIONED_HASH_VERSION_KZG {
102            return Err(InvalidTransaction::BlobVersionNotSupported);
103        }
104    }
105
106    // Ensure the total blob gas spent is at most equal to the limit
107    // assert blob_gas_used <= MAX_BLOB_GAS_PER_BLOCK
108    if let Some(max_blobs) = max_blobs {
109        if blobs.len() > max_blobs as usize {
110            return Err(InvalidTransaction::TooManyBlobs {
111                have: blobs.len(),
112                max: max_blobs as usize,
113            });
114        }
115    }
116    Ok(())
117}
118
119/// Validate transaction against block and configuration for mainnet.
120pub fn validate_tx_env<CTX: ContextTr>(
121    context: CTX,
122    spec_id: SpecId,
123) -> Result<(), InvalidTransaction> {
124    // Check if the transaction's chain id is correct
125    let tx = context.tx();
126    let tx_type = tx.tx_type();
127
128    let base_fee = if context.cfg().is_base_fee_check_disabled() {
129        None
130    } else {
131        Some(context.block().basefee() as u128)
132    };
133
134    let tx_type = TransactionType::from(tx_type);
135
136    // Check chain_id if config is enabled.
137    // EIP-155: Simple replay attack protection
138    if context.cfg().tx_chain_id_check() {
139        if let Some(chain_id) = tx.chain_id() {
140            if chain_id != context.cfg().chain_id() {
141                return Err(InvalidTransaction::InvalidChainId);
142            }
143        } else if !tx_type.is_legacy() && !tx_type.is_custom() {
144            // Legacy transaction are the only one that can omit chain_id.
145            return Err(InvalidTransaction::MissingChainId);
146        }
147    }
148
149    // tx gas cap is not enforced if state gas is enabled.
150    if !context.cfg().is_amsterdam_eip8037_enabled() {
151        // EIP-7825: Transaction Gas Limit Cap
152        let cap = context.cfg().tx_gas_limit_cap();
153        if tx.gas_limit() > cap {
154            return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
155                gas_limit: tx.gas_limit(),
156                cap,
157            });
158        }
159    }
160
161    let disable_priority_fee_check = context.cfg().is_priority_fee_check_disabled();
162
163    match tx_type {
164        TransactionType::Legacy => {
165            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
166        }
167        TransactionType::Eip2930 => {
168            // Enabled in BERLIN hardfork
169            if !spec_id.is_enabled_in(SpecId::BERLIN) {
170                return Err(InvalidTransaction::Eip2930NotSupported);
171            }
172            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
173        }
174        TransactionType::Eip1559 => {
175            if !spec_id.is_enabled_in(SpecId::LONDON) {
176                return Err(InvalidTransaction::Eip1559NotSupported);
177            }
178            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
179        }
180        TransactionType::Eip4844 => {
181            if !spec_id.is_enabled_in(SpecId::CANCUN) {
182                return Err(InvalidTransaction::Eip4844NotSupported);
183            }
184
185            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
186
187            validate_eip4844_tx(
188                tx.blob_versioned_hashes(),
189                tx.max_fee_per_blob_gas(),
190                context.block().blob_gasprice().unwrap_or_default(),
191                context.cfg().max_blobs_per_tx(),
192            )?;
193        }
194        TransactionType::Eip7702 => {
195            // Check if EIP-7702 transaction is enabled.
196            if !spec_id.is_enabled_in(SpecId::PRAGUE) {
197                return Err(InvalidTransaction::Eip7702NotSupported);
198            }
199
200            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
201
202            let auth_list_len = tx.authorization_list_len();
203            // The transaction is considered invalid if the length of authorization_list is zero.
204            if auth_list_len == 0 {
205                return Err(InvalidTransaction::EmptyAuthorizationList);
206            }
207        }
208        TransactionType::Custom => {
209            // Custom transaction type check is not done here.
210        }
211    };
212
213    // Check if gas_limit is more than block_gas_limit
214    // TODO(eip8037) should we enforce to `min(tx.gas_limit(), 16M) < block.gas_limit`?
215    // This would enforce that regular gas is constrained.
216    if !context.cfg().is_block_gas_limit_disabled() && tx.gas_limit() > context.block().gas_limit()
217    {
218        return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
219    }
220
221    // EIP-3860: Limit and meter initcode. Still valid with EIP-7907 and increase of initcode size.
222    if spec_id.is_enabled_in(SpecId::SHANGHAI)
223        && tx.kind().is_create()
224        && tx.input().len() > context.cfg().max_initcode_size()
225    {
226        return Err(InvalidTransaction::CreateInitCodeSizeLimit);
227    }
228
229    // Check that the transaction's nonce is not at the maximum value.
230    // Incrementing the nonce would overflow. Can't happen in the real world.
231    if tx.nonce() == u64::MAX {
232        return Err(InvalidTransaction::NonceOverflowInTransaction);
233    }
234
235    Ok(())
236}
237
238/// Validate initial transaction gas.
239pub fn validate_initial_tx_gas(
240    tx: impl Transaction,
241    spec: SpecId,
242    is_eip7623_disabled: bool,
243    is_amsterdam_eip8037_enabled: bool,
244    tx_gas_limit_cap: u64,
245    cpsb: u64,
246) -> Result<InitialAndFloorGas, InvalidTransaction> {
247    let mut gas = calculate_initial_tx_gas_for_tx(&tx, spec, cpsb);
248
249    if is_eip7623_disabled {
250        gas.set_floor_gas(0);
251    }
252
253    // Additional check to see if limit is big enough to cover initial gas.
254    if gas.initial_total_gas() > tx.gas_limit() {
255        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
256            gas_limit: tx.gas_limit(),
257            initial_gas: gas.initial_total_gas(),
258        });
259    }
260
261    // EIP-7623: Increase calldata cost
262    // floor gas should be less than gas limit.
263    if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas() > tx.gas_limit() {
264        return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
265            gas_floor: gas.floor_gas(),
266            gas_limit: tx.gas_limit(),
267        });
268    };
269
270    // EIP-8037: Regular gas is capped at TX_MAX_GAS_LIMIT.
271    // Validate that both intrinsic regular gas and floor gas fit within the cap.
272    // State gas is excluded — it uses its own reservoir.
273    if is_amsterdam_eip8037_enabled && tx.gas_limit() > tx_gas_limit_cap {
274        let min_regular_gas = gas.initial_regular_gas().max(gas.floor_gas());
275        if min_regular_gas > tx_gas_limit_cap {
276            return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
277                gas_floor: min_regular_gas,
278                gas_limit: tx_gas_limit_cap,
279            });
280        }
281    }
282
283    Ok(gas)
284}
285
286#[cfg(test)]
287mod tests {
288    use crate::{api::ExecuteEvm, ExecuteCommitEvm, MainBuilder, MainContext};
289    use bytecode::opcode;
290    use context::{
291        result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction, Output},
292        Context, ContextTr, TxEnv,
293    };
294    use database::{CacheDB, EmptyDB};
295    use primitives::{address, eip3860, eip7954, hardfork::SpecId, Bytes, TxKind, B256};
296    use state::{AccountInfo, Bytecode};
297
298    fn deploy_contract(
299        bytecode: Bytes,
300        spec_id: Option<SpecId>,
301    ) -> Result<ExecutionResult, EVMError<core::convert::Infallible>> {
302        let ctx = Context::mainnet()
303            .modify_cfg_chained(|c| {
304                if let Some(spec_id) = spec_id {
305                    c.set_spec_and_mainnet_gas_params(spec_id);
306                }
307            })
308            .modify_block_chained(|block| block.gas_limit = 100_000_000)
309            .with_db(CacheDB::<EmptyDB>::default());
310
311        let mut evm = ctx.build_mainnet();
312        evm.transact_commit(
313            TxEnv::builder()
314                .kind(TxKind::Create)
315                .data(bytecode.clone())
316                .build()
317                .unwrap(),
318        )
319    }
320
321    #[test]
322    fn test_eip3860_initcode_size_limit_failure() {
323        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE + 1];
324        let bytecode: Bytes = large_bytecode.into();
325        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
326        assert!(matches!(
327            result,
328            Err(EVMError::Transaction(
329                InvalidTransaction::CreateInitCodeSizeLimit
330            ))
331        ));
332    }
333
334    #[test]
335    fn test_eip3860_initcode_size_limit_success_prague() {
336        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE];
337        let bytecode: Bytes = large_bytecode.into();
338        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
339        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
340    }
341
342    #[test]
343    fn test_eip7954_initcode_size_limit_failure_amsterdam() {
344        let large_bytecode = vec![opcode::STOP; eip7954::MAX_INITCODE_SIZE + 1];
345        let bytecode: Bytes = large_bytecode.into();
346        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
347        assert!(matches!(
348            result,
349            Err(EVMError::Transaction(
350                InvalidTransaction::CreateInitCodeSizeLimit
351            ))
352        ));
353    }
354
355    #[test]
356    fn test_eip7954_initcode_size_limit_success_amsterdam() {
357        let large_bytecode = vec![opcode::STOP; eip7954::MAX_INITCODE_SIZE];
358        let bytecode: Bytes = large_bytecode.into();
359        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
360        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
361    }
362
363    #[test]
364    fn test_eip7954_initcode_between_old_and_new_limit() {
365        // Size between old limit (0xC000) and new limit (0x10000):
366        // should fail pre-Amsterdam, succeed at Amsterdam
367        let size = eip3860::MAX_INITCODE_SIZE + 1; // 0xC001
368        let large_bytecode = vec![opcode::STOP; size];
369
370        // Pre-Amsterdam (Prague): should fail
371        let bytecode: Bytes = large_bytecode.clone().into();
372        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
373        assert!(matches!(
374            result,
375            Err(EVMError::Transaction(
376                InvalidTransaction::CreateInitCodeSizeLimit
377            ))
378        ));
379
380        // Amsterdam: should succeed
381        let bytecode: Bytes = large_bytecode.into();
382        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
383        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
384    }
385
386    #[test]
387    fn test_eip7954_code_size_limit_failure() {
388        // EIP-7954: MAX_CODE_SIZE = 0x8000
389        // use the simplest method to return a contract code size greater than 0x8000
390        // PUSH3 0x8001 (greater than 0x8000) - return size
391        // PUSH1 0x00 - memory position 0
392        // RETURN - return uninitialized memory, will be filled with 0
393        let init_code = vec![
394            0x62, 0x00, 0x80, 0x01, // PUSH3 0x8001 (greater than 0x8000)
395            0x60, 0x00, // PUSH1 0
396            0xf3, // RETURN
397        ];
398        let bytecode: Bytes = init_code.into();
399        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
400        assert!(
401            matches!(
402                result,
403                Ok(ExecutionResult::Halt {
404                    reason: HaltReason::CreateContractSizeLimit,
405                    ..
406                },)
407            ),
408            "{result:?}"
409        );
410    }
411
412    #[test]
413    fn test_eip170_code_size_limit_failure() {
414        // use the simplest method to return a contract code size greater than 0x6000
415        // PUSH3 0x6001 (greater than 0x6000) - return size
416        // PUSH1 0x00 - memory position 0
417        // RETURN - return uninitialized memory, will be filled with 0
418        let init_code = vec![
419            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (greater than 0x6000)
420            0x60, 0x00, // PUSH1 0
421            0xf3, // RETURN
422        ];
423        let bytecode: Bytes = init_code.into();
424        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
425        assert!(
426            matches!(
427                result,
428                Ok(ExecutionResult::Halt {
429                    reason: HaltReason::CreateContractSizeLimit,
430                    ..
431                },)
432            ),
433            "{result:?}"
434        );
435    }
436
437    #[test]
438    fn test_eip170_code_size_limit_success() {
439        // use the  simplest method to return a contract code size equal to 0x6000
440        // PUSH3 0x6000 - return size
441        // PUSH1 0x00 - memory position 0
442        // RETURN - return uninitialized memory, will be filled with 0
443        let init_code = vec![
444            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000
445            0x60, 0x00, // PUSH1 0
446            0xf3, // RETURN
447        ];
448        let bytecode: Bytes = init_code.into();
449        let result = deploy_contract(bytecode, None);
450        assert!(matches!(result, Ok(ExecutionResult::Success { .. },)));
451    }
452
453    #[test]
454    fn test_eip170_create_opcode_size_limit_failure() {
455        // 1. create a "factory" contract, which will use the CREATE opcode to create another large contract
456        // 2. because the sub contract exceeds the EIP-170 limit, the CREATE operation should fail
457
458        // the bytecode of the factory contract:
459        // PUSH1 0x01      - the value for MSTORE
460        // PUSH1 0x00      - the memory position
461        // MSTORE          - store a non-zero value at the beginning of memory
462
463        // PUSH3 0x6001    - the return size (exceeds 0x6000)
464        // PUSH1 0x00      - the memory offset
465        // PUSH1 0x00      - the amount of ETH sent
466        // CREATE          - create contract instruction (create contract from current memory)
467
468        // PUSH1 0x00      - the return value storage position
469        // MSTORE          - store the address returned by CREATE to the memory position 0
470        // PUSH1 0x20      - the return size (32 bytes)
471        // PUSH1 0x00      - the return offset
472        // RETURN          - return the result
473
474        let factory_code = vec![
475            // 1. store a non-zero value at the beginning of memory
476            0x60, 0x01, // PUSH1 0x01
477            0x60, 0x00, // PUSH1 0x00
478            0x52, // MSTORE
479            // 2. prepare to create a large contract
480            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (exceeds 0x6000)
481            0x60, 0x00, // PUSH1 0x00 (the memory offset)
482            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
483            0xf0, // CREATE
484            // 3. store the address returned by CREATE to the memory position 0
485            0x60, 0x00, // PUSH1 0x00
486            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
487            // 4. return the result
488            0x60, 0x20, // PUSH1 0x20 (32 bytes)
489            0x60, 0x00, // PUSH1 0x00
490            0xf3, // RETURN
491        ];
492
493        // deploy factory contract
494        let factory_bytecode: Bytes = factory_code.into();
495        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
496            .expect("factory contract deployment failed");
497
498        // get factory contract address
499        let factory_address = match &factory_result {
500            ExecutionResult::Success {
501                output: Output::Create(_, Some(addr)),
502                ..
503            } => *addr,
504            _ => panic!("factory contract deployment failed: {factory_result:?}"),
505        };
506
507        // call factory contract to create sub contract
508        let tx_caller = address!("0x0000000000000000000000000000000000100000");
509        let call_result = Context::mainnet()
510            .with_db(CacheDB::<EmptyDB>::default())
511            .build_mainnet()
512            .transact_commit(
513                TxEnv::builder()
514                    .caller(tx_caller)
515                    .kind(TxKind::Call(factory_address))
516                    .data(Bytes::new())
517                    .build()
518                    .unwrap(),
519            )
520            .expect("call factory contract failed");
521
522        match &call_result {
523            ExecutionResult::Success { output, .. } => match output {
524                Output::Call(bytes) => {
525                    if !bytes.is_empty() {
526                        assert!(
527                            bytes.iter().all(|&b| b == 0),
528                            "When CREATE operation failed, it should return all zero address"
529                        );
530                    }
531                }
532                _ => panic!("unexpected output type"),
533            },
534            _ => panic!("execution result is not Success"),
535        }
536    }
537
538    #[test]
539    fn test_eip170_create_opcode_size_limit_success() {
540        // 1. create a "factory" contract, which will use the CREATE opcode to create another contract
541        // 2. the sub contract generated by the factory contract does not exceed the EIP-170 limit, so it should be created successfully
542
543        // the bytecode of the factory contract:
544        // PUSH1 0x01      - the value for MSTORE
545        // PUSH1 0x00      - the memory position
546        // MSTORE          - store a non-zero value at the beginning of memory
547
548        // PUSH3 0x6000    - the return size (0x6000)
549        // PUSH1 0x00      - the memory offset
550        // PUSH1 0x00      - the amount of ETH sent
551        // CREATE          - create contract instruction (create contract from current memory)
552
553        // PUSH1 0x00      - the return value storage position
554        // MSTORE          - store the address returned by CREATE to the memory position 0
555        // PUSH1 0x20      - the return size (32 bytes)
556        // PUSH1 0x00      - the return offset
557        // RETURN          - return the result
558
559        let factory_code = vec![
560            // 1. store a non-zero value at the beginning of memory
561            0x60, 0x01, // PUSH1 0x01
562            0x60, 0x00, // PUSH1 0x00
563            0x52, // MSTORE
564            // 2. prepare to create a contract
565            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000 (0x6000)
566            0x60, 0x00, // PUSH1 0x00 (the memory offset)
567            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
568            0xf0, // CREATE
569            // 3. store the address returned by CREATE to the memory position 0
570            0x60, 0x00, // PUSH1 0x00
571            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
572            // 4. return the result
573            0x60, 0x20, // PUSH1 0x20 (32 bytes)
574            0x60, 0x00, // PUSH1 0x00
575            0xf3, // RETURN
576        ];
577
578        // deploy factory contract
579        let factory_bytecode: Bytes = factory_code.into();
580        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
581            .expect("factory contract deployment failed");
582        // get factory contract address
583        let factory_address = match &factory_result {
584            ExecutionResult::Success {
585                output: Output::Create(_, Some(addr)),
586                ..
587            } => *addr,
588            _ => panic!("factory contract deployment failed: {factory_result:?}"),
589        };
590
591        // call factory contract to create sub contract
592        let tx_caller = address!("0x0000000000000000000000000000000000100000");
593        let call_result = Context::mainnet()
594            .with_db(CacheDB::<EmptyDB>::default())
595            .build_mainnet()
596            .transact_commit(
597                TxEnv::builder()
598                    .caller(tx_caller)
599                    .kind(TxKind::Call(factory_address))
600                    .data(Bytes::new())
601                    .build()
602                    .unwrap(),
603            )
604            .expect("call factory contract failed");
605
606        match &call_result {
607            ExecutionResult::Success { output, .. } => {
608                match output {
609                    Output::Call(bytes) => {
610                        // check if CREATE operation is successful (return non-zero address)
611                        if !bytes.is_empty() {
612                            assert!(bytes.iter().any(|&b| b != 0), "create sub contract failed");
613                        }
614                    }
615                    _ => panic!("unexpected output type"),
616                }
617            }
618            _ => panic!("execution result is not Success"),
619        }
620    }
621
622    #[test]
623    fn test_transact_many_with_transaction_index_error() {
624        use context::result::TransactionIndexedError;
625
626        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
627        let mut evm = ctx.build_mainnet();
628
629        // Create a transaction that will fail (invalid gas limit)
630        let invalid_tx = TxEnv::builder()
631            .gas_limit(0) // This will cause a validation error
632            .build()
633            .unwrap();
634
635        // Create a valid transaction
636        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
637
638        // Test that the first transaction fails with index 0
639        let result = evm.transact_many([invalid_tx.clone()].into_iter());
640        assert!(matches!(
641            result,
642            Err(TransactionIndexedError {
643                transaction_index: 0,
644                ..
645            })
646        ));
647
648        // Test that the second transaction fails with index 1
649        let result = evm.transact_many([valid_tx, invalid_tx].into_iter());
650        assert!(matches!(
651            result,
652            Err(TransactionIndexedError {
653                transaction_index: 1,
654                ..
655            })
656        ));
657    }
658
659    #[test]
660    fn test_transact_many_success() {
661        use primitives::{address, U256};
662
663        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
664        let mut evm = ctx.build_mainnet();
665
666        // Add balance to the caller account
667        let caller = address!("0x0000000000000000000000000000000000000001");
668        evm.db_mut().insert_account_info(
669            caller,
670            AccountInfo::new(
671                U256::from(1000000000000000000u64),
672                0,
673                B256::ZERO,
674                Bytecode::new(),
675            ),
676        );
677
678        // Create valid transactions with proper data
679        let tx1 = TxEnv::builder()
680            .caller(caller)
681            .gas_limit(100000)
682            .gas_price(20_000_000_000u128)
683            .nonce(0)
684            .build()
685            .unwrap();
686
687        let tx2 = TxEnv::builder()
688            .caller(caller)
689            .gas_limit(100000)
690            .gas_price(20_000_000_000u128)
691            .nonce(1)
692            .build()
693            .unwrap();
694
695        // Test that all transactions succeed
696        let result = evm.transact_many([tx1, tx2].into_iter());
697        if let Err(e) = &result {
698            println!("Error: {e:?}");
699        }
700        let outputs = result.expect("All transactions should succeed");
701        assert_eq!(outputs.len(), 2);
702    }
703
704    #[test]
705    fn test_transact_many_finalize_with_error() {
706        use context::result::TransactionIndexedError;
707
708        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
709        let mut evm = ctx.build_mainnet();
710
711        // Create transactions where the second one fails
712        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
713
714        let invalid_tx = TxEnv::builder()
715            .gas_limit(0) // This will cause a validation error
716            .build()
717            .unwrap();
718
719        // Test that transact_many_finalize returns the error with correct index
720        let result = evm.transact_many_finalize([valid_tx, invalid_tx].into_iter());
721        assert!(matches!(
722            result,
723            Err(TransactionIndexedError {
724                transaction_index: 1,
725                ..
726            })
727        ));
728    }
729
730    #[test]
731    fn test_transact_many_commit_with_error() {
732        use context::result::TransactionIndexedError;
733
734        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
735        let mut evm = ctx.build_mainnet();
736
737        // Create transactions where the first one fails
738        let invalid_tx = TxEnv::builder()
739            .gas_limit(0) // This will cause a validation error
740            .build()
741            .unwrap();
742
743        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
744
745        // Test that transact_many_commit returns the error with correct index
746        let result = evm.transact_many_commit([invalid_tx, valid_tx].into_iter());
747        assert!(matches!(
748            result,
749            Err(TransactionIndexedError {
750                transaction_index: 0,
751                ..
752            })
753        ));
754    }
755}