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) -> Result<InitialAndFloorGas, InvalidTransaction> {
246    let mut gas = calculate_initial_tx_gas_for_tx(&tx, spec);
247
248    if is_eip7623_disabled {
249        gas.floor_gas = 0
250    }
251
252    // Additional check to see if limit is big enough to cover initial gas.
253    if gas.initial_total_gas > tx.gas_limit() {
254        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
255            gas_limit: tx.gas_limit(),
256            initial_gas: gas.initial_total_gas,
257        });
258    }
259
260    // EIP-7623: Increase calldata cost
261    // floor gas should be less than gas limit.
262    if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas > tx.gas_limit() {
263        return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
264            gas_floor: gas.floor_gas,
265            gas_limit: tx.gas_limit(),
266        });
267    };
268
269    // EIP-8037: Regular gas is capped at TX_MAX_GAS_LIMIT.
270    // Validate that both intrinsic regular gas and floor gas fit within the cap.
271    // State gas is excluded — it uses its own reservoir.
272    if is_amsterdam_eip8037_enabled && tx.gas_limit() > tx_gas_limit_cap {
273        let min_regular_gas = gas.initial_regular_gas().max(gas.floor_gas);
274        if min_regular_gas > tx_gas_limit_cap {
275            return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
276                gas_floor: min_regular_gas,
277                gas_limit: tx_gas_limit_cap,
278            });
279        }
280    }
281
282    Ok(gas)
283}
284
285#[cfg(test)]
286mod tests {
287    use crate::{api::ExecuteEvm, ExecuteCommitEvm, MainBuilder, MainContext};
288    use bytecode::opcode;
289    use context::{
290        result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction, Output},
291        Context, ContextTr, TxEnv,
292    };
293    use database::{CacheDB, EmptyDB};
294    use primitives::{address, eip3860, eip7954, hardfork::SpecId, Bytes, TxKind, B256};
295    use state::{AccountInfo, Bytecode};
296
297    fn deploy_contract(
298        bytecode: Bytes,
299        spec_id: Option<SpecId>,
300    ) -> Result<ExecutionResult, EVMError<core::convert::Infallible>> {
301        let ctx = Context::mainnet()
302            .modify_cfg_chained(|c| {
303                if let Some(spec_id) = spec_id {
304                    c.set_spec_and_mainnet_gas_params(spec_id);
305                }
306            })
307            .with_db(CacheDB::<EmptyDB>::default());
308
309        let mut evm = ctx.build_mainnet();
310        evm.transact_commit(
311            TxEnv::builder()
312                .kind(TxKind::Create)
313                .data(bytecode.clone())
314                .build()
315                .unwrap(),
316        )
317    }
318
319    #[test]
320    fn test_eip3860_initcode_size_limit_failure() {
321        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE + 1];
322        let bytecode: Bytes = large_bytecode.into();
323        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
324        assert!(matches!(
325            result,
326            Err(EVMError::Transaction(
327                InvalidTransaction::CreateInitCodeSizeLimit
328            ))
329        ));
330    }
331
332    #[test]
333    fn test_eip3860_initcode_size_limit_success_prague() {
334        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE];
335        let bytecode: Bytes = large_bytecode.into();
336        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
337        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
338    }
339
340    #[test]
341    fn test_eip7954_initcode_size_limit_failure_amsterdam() {
342        let large_bytecode = vec![opcode::STOP; eip7954::MAX_INITCODE_SIZE + 1];
343        let bytecode: Bytes = large_bytecode.into();
344        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
345        assert!(matches!(
346            result,
347            Err(EVMError::Transaction(
348                InvalidTransaction::CreateInitCodeSizeLimit
349            ))
350        ));
351    }
352
353    #[test]
354    fn test_eip7954_initcode_size_limit_success_amsterdam() {
355        let large_bytecode = vec![opcode::STOP; eip7954::MAX_INITCODE_SIZE];
356        let bytecode: Bytes = large_bytecode.into();
357        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
358        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
359    }
360
361    #[test]
362    fn test_eip7954_initcode_between_old_and_new_limit() {
363        // Size between old limit (0xC000) and new limit (0x10000):
364        // should fail pre-Amsterdam, succeed at Amsterdam
365        let size = eip3860::MAX_INITCODE_SIZE + 1; // 0xC001
366        let large_bytecode = vec![opcode::STOP; size];
367
368        // Pre-Amsterdam (Prague): should fail
369        let bytecode: Bytes = large_bytecode.clone().into();
370        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
371        assert!(matches!(
372            result,
373            Err(EVMError::Transaction(
374                InvalidTransaction::CreateInitCodeSizeLimit
375            ))
376        ));
377
378        // Amsterdam: should succeed
379        let bytecode: Bytes = large_bytecode.into();
380        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
381        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
382    }
383
384    #[test]
385    fn test_eip7954_code_size_limit_failure() {
386        // EIP-7954: MAX_CODE_SIZE = 0x8000
387        // use the simplest method to return a contract code size greater than 0x8000
388        // PUSH3 0x8001 (greater than 0x8000) - return size
389        // PUSH1 0x00 - memory position 0
390        // RETURN - return uninitialized memory, will be filled with 0
391        let init_code = vec![
392            0x62, 0x00, 0x80, 0x01, // PUSH3 0x8001 (greater than 0x8000)
393            0x60, 0x00, // PUSH1 0
394            0xf3, // RETURN
395        ];
396        let bytecode: Bytes = init_code.into();
397        let result = deploy_contract(bytecode, Some(SpecId::AMSTERDAM));
398        assert!(
399            matches!(
400                result,
401                Ok(ExecutionResult::Halt {
402                    reason: HaltReason::CreateContractSizeLimit,
403                    ..
404                },)
405            ),
406            "{result:?}"
407        );
408    }
409
410    #[test]
411    fn test_eip170_code_size_limit_failure() {
412        // use the simplest method to return a contract code size greater than 0x6000
413        // PUSH3 0x6001 (greater than 0x6000) - return size
414        // PUSH1 0x00 - memory position 0
415        // RETURN - return uninitialized memory, will be filled with 0
416        let init_code = vec![
417            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (greater than 0x6000)
418            0x60, 0x00, // PUSH1 0
419            0xf3, // RETURN
420        ];
421        let bytecode: Bytes = init_code.into();
422        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
423        assert!(
424            matches!(
425                result,
426                Ok(ExecutionResult::Halt {
427                    reason: HaltReason::CreateContractSizeLimit,
428                    ..
429                },)
430            ),
431            "{result:?}"
432        );
433    }
434
435    #[test]
436    fn test_eip170_code_size_limit_success() {
437        // use the  simplest method to return a contract code size equal to 0x6000
438        // PUSH3 0x6000 - return size
439        // PUSH1 0x00 - memory position 0
440        // RETURN - return uninitialized memory, will be filled with 0
441        let init_code = vec![
442            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000
443            0x60, 0x00, // PUSH1 0
444            0xf3, // RETURN
445        ];
446        let bytecode: Bytes = init_code.into();
447        let result = deploy_contract(bytecode, None);
448        assert!(matches!(result, Ok(ExecutionResult::Success { .. },)));
449    }
450
451    #[test]
452    fn test_eip170_create_opcode_size_limit_failure() {
453        // 1. create a "factory" contract, which will use the CREATE opcode to create another large contract
454        // 2. because the sub contract exceeds the EIP-170 limit, the CREATE operation should fail
455
456        // the bytecode of the factory contract:
457        // PUSH1 0x01      - the value for MSTORE
458        // PUSH1 0x00      - the memory position
459        // MSTORE          - store a non-zero value at the beginning of memory
460
461        // PUSH3 0x6001    - the return size (exceeds 0x6000)
462        // PUSH1 0x00      - the memory offset
463        // PUSH1 0x00      - the amount of ETH sent
464        // CREATE          - create contract instruction (create contract from current memory)
465
466        // PUSH1 0x00      - the return value storage position
467        // MSTORE          - store the address returned by CREATE to the memory position 0
468        // PUSH1 0x20      - the return size (32 bytes)
469        // PUSH1 0x00      - the return offset
470        // RETURN          - return the result
471
472        let factory_code = vec![
473            // 1. store a non-zero value at the beginning of memory
474            0x60, 0x01, // PUSH1 0x01
475            0x60, 0x00, // PUSH1 0x00
476            0x52, // MSTORE
477            // 2. prepare to create a large contract
478            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (exceeds 0x6000)
479            0x60, 0x00, // PUSH1 0x00 (the memory offset)
480            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
481            0xf0, // CREATE
482            // 3. store the address returned by CREATE to the memory position 0
483            0x60, 0x00, // PUSH1 0x00
484            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
485            // 4. return the result
486            0x60, 0x20, // PUSH1 0x20 (32 bytes)
487            0x60, 0x00, // PUSH1 0x00
488            0xf3, // RETURN
489        ];
490
491        // deploy factory contract
492        let factory_bytecode: Bytes = factory_code.into();
493        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
494            .expect("factory contract deployment failed");
495
496        // get factory contract address
497        let factory_address = match &factory_result {
498            ExecutionResult::Success {
499                output: Output::Create(_, Some(addr)),
500                ..
501            } => *addr,
502            _ => panic!("factory contract deployment failed: {factory_result:?}"),
503        };
504
505        // call factory contract to create sub contract
506        let tx_caller = address!("0x0000000000000000000000000000000000100000");
507        let call_result = Context::mainnet()
508            .with_db(CacheDB::<EmptyDB>::default())
509            .build_mainnet()
510            .transact_commit(
511                TxEnv::builder()
512                    .caller(tx_caller)
513                    .kind(TxKind::Call(factory_address))
514                    .data(Bytes::new())
515                    .build()
516                    .unwrap(),
517            )
518            .expect("call factory contract failed");
519
520        match &call_result {
521            ExecutionResult::Success { output, .. } => match output {
522                Output::Call(bytes) => {
523                    if !bytes.is_empty() {
524                        assert!(
525                            bytes.iter().all(|&b| b == 0),
526                            "When CREATE operation failed, it should return all zero address"
527                        );
528                    }
529                }
530                _ => panic!("unexpected output type"),
531            },
532            _ => panic!("execution result is not Success"),
533        }
534    }
535
536    #[test]
537    fn test_eip170_create_opcode_size_limit_success() {
538        // 1. create a "factory" contract, which will use the CREATE opcode to create another contract
539        // 2. the sub contract generated by the factory contract does not exceed the EIP-170 limit, so it should be created successfully
540
541        // the bytecode of the factory contract:
542        // PUSH1 0x01      - the value for MSTORE
543        // PUSH1 0x00      - the memory position
544        // MSTORE          - store a non-zero value at the beginning of memory
545
546        // PUSH3 0x6000    - the return size (0x6000)
547        // PUSH1 0x00      - the memory offset
548        // PUSH1 0x00      - the amount of ETH sent
549        // CREATE          - create contract instruction (create contract from current memory)
550
551        // PUSH1 0x00      - the return value storage position
552        // MSTORE          - store the address returned by CREATE to the memory position 0
553        // PUSH1 0x20      - the return size (32 bytes)
554        // PUSH1 0x00      - the return offset
555        // RETURN          - return the result
556
557        let factory_code = vec![
558            // 1. store a non-zero value at the beginning of memory
559            0x60, 0x01, // PUSH1 0x01
560            0x60, 0x00, // PUSH1 0x00
561            0x52, // MSTORE
562            // 2. prepare to create a contract
563            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000 (0x6000)
564            0x60, 0x00, // PUSH1 0x00 (the memory offset)
565            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
566            0xf0, // CREATE
567            // 3. store the address returned by CREATE to the memory position 0
568            0x60, 0x00, // PUSH1 0x00
569            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
570            // 4. return the result
571            0x60, 0x20, // PUSH1 0x20 (32 bytes)
572            0x60, 0x00, // PUSH1 0x00
573            0xf3, // RETURN
574        ];
575
576        // deploy factory contract
577        let factory_bytecode: Bytes = factory_code.into();
578        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
579            .expect("factory contract deployment failed");
580        // get factory contract address
581        let factory_address = match &factory_result {
582            ExecutionResult::Success {
583                output: Output::Create(_, Some(addr)),
584                ..
585            } => *addr,
586            _ => panic!("factory contract deployment failed: {factory_result:?}"),
587        };
588
589        // call factory contract to create sub contract
590        let tx_caller = address!("0x0000000000000000000000000000000000100000");
591        let call_result = Context::mainnet()
592            .with_db(CacheDB::<EmptyDB>::default())
593            .build_mainnet()
594            .transact_commit(
595                TxEnv::builder()
596                    .caller(tx_caller)
597                    .kind(TxKind::Call(factory_address))
598                    .data(Bytes::new())
599                    .build()
600                    .unwrap(),
601            )
602            .expect("call factory contract failed");
603
604        match &call_result {
605            ExecutionResult::Success { output, .. } => {
606                match output {
607                    Output::Call(bytes) => {
608                        // check if CREATE operation is successful (return non-zero address)
609                        if !bytes.is_empty() {
610                            assert!(bytes.iter().any(|&b| b != 0), "create sub contract failed");
611                        }
612                    }
613                    _ => panic!("unexpected output type"),
614                }
615            }
616            _ => panic!("execution result is not Success"),
617        }
618    }
619
620    #[test]
621    fn test_transact_many_with_transaction_index_error() {
622        use context::result::TransactionIndexedError;
623
624        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
625        let mut evm = ctx.build_mainnet();
626
627        // Create a transaction that will fail (invalid gas limit)
628        let invalid_tx = TxEnv::builder()
629            .gas_limit(0) // This will cause a validation error
630            .build()
631            .unwrap();
632
633        // Create a valid transaction
634        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
635
636        // Test that the first transaction fails with index 0
637        let result = evm.transact_many([invalid_tx.clone()].into_iter());
638        assert!(matches!(
639            result,
640            Err(TransactionIndexedError {
641                transaction_index: 0,
642                ..
643            })
644        ));
645
646        // Test that the second transaction fails with index 1
647        let result = evm.transact_many([valid_tx, invalid_tx].into_iter());
648        assert!(matches!(
649            result,
650            Err(TransactionIndexedError {
651                transaction_index: 1,
652                ..
653            })
654        ));
655    }
656
657    #[test]
658    fn test_transact_many_success() {
659        use primitives::{address, U256};
660
661        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
662        let mut evm = ctx.build_mainnet();
663
664        // Add balance to the caller account
665        let caller = address!("0x0000000000000000000000000000000000000001");
666        evm.db_mut().insert_account_info(
667            caller,
668            AccountInfo::new(
669                U256::from(1000000000000000000u64),
670                0,
671                B256::ZERO,
672                Bytecode::new(),
673            ),
674        );
675
676        // Create valid transactions with proper data
677        let tx1 = TxEnv::builder()
678            .caller(caller)
679            .gas_limit(100000)
680            .gas_price(20_000_000_000u128)
681            .nonce(0)
682            .build()
683            .unwrap();
684
685        let tx2 = TxEnv::builder()
686            .caller(caller)
687            .gas_limit(100000)
688            .gas_price(20_000_000_000u128)
689            .nonce(1)
690            .build()
691            .unwrap();
692
693        // Test that all transactions succeed
694        let result = evm.transact_many([tx1, tx2].into_iter());
695        if let Err(e) = &result {
696            println!("Error: {e:?}");
697        }
698        let outputs = result.expect("All transactions should succeed");
699        assert_eq!(outputs.len(), 2);
700    }
701
702    #[test]
703    fn test_transact_many_finalize_with_error() {
704        use context::result::TransactionIndexedError;
705
706        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
707        let mut evm = ctx.build_mainnet();
708
709        // Create transactions where the second one fails
710        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
711
712        let invalid_tx = TxEnv::builder()
713            .gas_limit(0) // This will cause a validation error
714            .build()
715            .unwrap();
716
717        // Test that transact_many_finalize returns the error with correct index
718        let result = evm.transact_many_finalize([valid_tx, invalid_tx].into_iter());
719        assert!(matches!(
720            result,
721            Err(TransactionIndexedError {
722                transaction_index: 1,
723                ..
724            })
725        ));
726    }
727
728    #[test]
729    fn test_transact_many_commit_with_error() {
730        use context::result::TransactionIndexedError;
731
732        let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
733        let mut evm = ctx.build_mainnet();
734
735        // Create transactions where the first one fails
736        let invalid_tx = TxEnv::builder()
737            .gas_limit(0) // This will cause a validation error
738            .build()
739            .unwrap();
740
741        let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();
742
743        // Test that transact_many_commit returns the error with correct index
744        let result = evm.transact_many_commit([invalid_tx, valid_tx].into_iter());
745        assert!(matches!(
746            result,
747            Err(TransactionIndexedError {
748                transaction_index: 0,
749                ..
750            })
751        ));
752    }
753}