Skip to main content

revm_handler/
validation.rs

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