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::gas::{self, 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, InvalidTransaction>(context, spec).map_err(Into::into)
24}
25
26/// Validate transaction that has EIP-1559 priority fee
27pub fn validate_priority_fee_tx(
28    max_fee: u128,
29    max_priority_fee: u128,
30    base_fee: Option<u128>,
31    disable_priority_fee_check: bool,
32) -> Result<(), InvalidTransaction> {
33    if !disable_priority_fee_check && max_priority_fee > max_fee {
34        // Or gas_max_fee for eip1559
35        return Err(InvalidTransaction::PriorityFeeGreaterThanMaxFee);
36    }
37
38    // Check minimal cost against basefee
39    if let Some(base_fee) = base_fee {
40        let effective_gas_price = cmp::min(max_fee, base_fee.saturating_add(max_priority_fee));
41        if effective_gas_price < base_fee {
42            return Err(InvalidTransaction::GasPriceLessThanBasefee);
43        }
44    }
45
46    Ok(())
47}
48
49/// Validate EIP-4844 transaction.
50pub fn validate_eip4844_tx(
51    blobs: &[B256],
52    max_blob_fee: u128,
53    block_blob_gas_price: u128,
54    max_blobs: Option<u64>,
55) -> Result<(), InvalidTransaction> {
56    // Ensure that the user was willing to at least pay the current blob gasprice
57    if block_blob_gas_price > max_blob_fee {
58        return Err(InvalidTransaction::BlobGasPriceGreaterThanMax);
59    }
60
61    // There must be at least one blob
62    if blobs.is_empty() {
63        return Err(InvalidTransaction::EmptyBlobs);
64    }
65
66    // All versioned blob hashes must start with VERSIONED_HASH_VERSION_KZG
67    for blob in blobs {
68        if blob[0] != eip4844::VERSIONED_HASH_VERSION_KZG {
69            return Err(InvalidTransaction::BlobVersionNotSupported);
70        }
71    }
72
73    // Ensure the total blob gas spent is at most equal to the limit
74    // assert blob_gas_used <= MAX_BLOB_GAS_PER_BLOCK
75    if let Some(max_blobs) = max_blobs {
76        if blobs.len() > max_blobs as usize {
77            return Err(InvalidTransaction::TooManyBlobs {
78                have: blobs.len(),
79                max: max_blobs as usize,
80            });
81        }
82    }
83    Ok(())
84}
85
86/// Validate transaction against block and configuration for mainnet.
87pub fn validate_tx_env<CTX: ContextTr, Error>(
88    context: CTX,
89    spec_id: SpecId,
90) -> Result<(), InvalidTransaction> {
91    // Check if the transaction's chain id is correct
92    let tx_type = context.tx().tx_type();
93    let tx = context.tx();
94
95    let base_fee = if context.cfg().is_base_fee_check_disabled() {
96        None
97    } else {
98        Some(context.block().basefee() as u128)
99    };
100
101    let tx_type = TransactionType::from(tx_type);
102
103    // Check chain_id if config is enabled.
104    // EIP-155: Simple replay attack protection
105    if context.cfg().tx_chain_id_check() {
106        if let Some(chain_id) = tx.chain_id() {
107            if chain_id != context.cfg().chain_id() {
108                return Err(InvalidTransaction::InvalidChainId);
109            }
110        } else if !tx_type.is_legacy() && !tx_type.is_custom() {
111            // Legacy transaction are the only one that can omit chain_id.
112            return Err(InvalidTransaction::MissingChainId);
113        }
114    }
115
116    // EIP-7825: Transaction Gas Limit Cap
117    let cap = context.cfg().tx_gas_limit_cap();
118    if tx.gas_limit() > cap {
119        return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
120            gas_limit: tx.gas_limit(),
121            cap,
122        });
123    }
124
125    let disable_priority_fee_check = context.cfg().is_priority_fee_check_disabled();
126
127    match tx_type {
128        TransactionType::Legacy => {
129            // Gas price must be at least the basefee.
130            if let Some(base_fee) = base_fee {
131                if tx.gas_price() < base_fee {
132                    return Err(InvalidTransaction::GasPriceLessThanBasefee);
133                }
134            }
135        }
136        TransactionType::Eip2930 => {
137            // Enabled in BERLIN hardfork
138            if !spec_id.is_enabled_in(SpecId::BERLIN) {
139                return Err(InvalidTransaction::Eip2930NotSupported);
140            }
141
142            // Gas price must be at least the basefee.
143            if let Some(base_fee) = base_fee {
144                if tx.gas_price() < base_fee {
145                    return Err(InvalidTransaction::GasPriceLessThanBasefee);
146                }
147            }
148        }
149        TransactionType::Eip1559 => {
150            if !spec_id.is_enabled_in(SpecId::LONDON) {
151                return Err(InvalidTransaction::Eip1559NotSupported);
152            }
153            validate_priority_fee_tx(
154                tx.max_fee_per_gas(),
155                tx.max_priority_fee_per_gas().unwrap_or_default(),
156                base_fee,
157                disable_priority_fee_check,
158            )?;
159        }
160        TransactionType::Eip4844 => {
161            if !spec_id.is_enabled_in(SpecId::CANCUN) {
162                return Err(InvalidTransaction::Eip4844NotSupported);
163            }
164
165            validate_priority_fee_tx(
166                tx.max_fee_per_gas(),
167                tx.max_priority_fee_per_gas().unwrap_or_default(),
168                base_fee,
169                disable_priority_fee_check,
170            )?;
171
172            validate_eip4844_tx(
173                tx.blob_versioned_hashes(),
174                tx.max_fee_per_blob_gas(),
175                context.block().blob_gasprice().unwrap_or_default(),
176                context.cfg().max_blobs_per_tx(),
177            )?;
178        }
179        TransactionType::Eip7702 => {
180            // Check if EIP-7702 transaction is enabled.
181            if !spec_id.is_enabled_in(SpecId::PRAGUE) {
182                return Err(InvalidTransaction::Eip7702NotSupported);
183            }
184
185            validate_priority_fee_tx(
186                tx.max_fee_per_gas(),
187                tx.max_priority_fee_per_gas().unwrap_or_default(),
188                base_fee,
189                disable_priority_fee_check,
190            )?;
191
192            let auth_list_len = tx.authorization_list_len();
193            // The transaction is considered invalid if the length of authorization_list is zero.
194            if auth_list_len == 0 {
195                return Err(InvalidTransaction::EmptyAuthorizationList);
196            }
197        }
198        TransactionType::Custom => {
199            // Custom transaction type check is not done here.
200        }
201    };
202
203    // Check if gas_limit is more than block_gas_limit
204    if !context.cfg().is_block_gas_limit_disabled() && tx.gas_limit() > context.block().gas_limit()
205    {
206        return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
207    }
208
209    // EIP-3860: Limit and meter initcode. Still valid with EIP-7907 and increase of initcode size.
210    if spec_id.is_enabled_in(SpecId::SHANGHAI)
211        && tx.kind().is_create()
212        && context.tx().input().len() > context.cfg().max_initcode_size()
213    {
214        return Err(InvalidTransaction::CreateInitCodeSizeLimit);
215    }
216
217    Ok(())
218}
219
220/// Validate initial transaction gas.
221pub fn validate_initial_tx_gas(
222    tx: impl Transaction,
223    spec: SpecId,
224) -> Result<InitialAndFloorGas, InvalidTransaction> {
225    let gas = gas::calculate_initial_tx_gas_for_tx(&tx, spec);
226
227    // Additional check to see if limit is big enough to cover initial gas.
228    if gas.initial_gas > tx.gas_limit() {
229        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
230            gas_limit: tx.gas_limit(),
231            initial_gas: gas.initial_gas,
232        });
233    }
234
235    // EIP-7623: Increase calldata cost
236    // floor gas should be less than gas limit.
237    if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas > tx.gas_limit() {
238        return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
239            gas_floor: gas.floor_gas,
240            gas_limit: tx.gas_limit(),
241        });
242    };
243
244    Ok(gas)
245}
246
247#[cfg(test)]
248mod tests {
249    use crate::{ExecuteCommitEvm, MainBuilder, MainContext};
250    use bytecode::opcode;
251    use context::{
252        result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction, Output},
253        Context, TxEnv,
254    };
255    use database::{CacheDB, EmptyDB};
256    use primitives::{address, eip3860, eip7907, hardfork::SpecId, Bytes, TxKind};
257
258    fn deploy_contract(
259        bytecode: Bytes,
260        spec_id: Option<SpecId>,
261    ) -> Result<ExecutionResult, EVMError<core::convert::Infallible>> {
262        let ctx = Context::mainnet()
263            .modify_cfg_chained(|c| {
264                if let Some(spec_id) = spec_id {
265                    c.spec = spec_id;
266                }
267            })
268            .with_db(CacheDB::<EmptyDB>::default());
269
270        let mut evm = ctx.build_mainnet();
271        evm.transact_commit(
272            TxEnv::builder()
273                .kind(TxKind::Create)
274                .data(bytecode.clone())
275                .build()
276                .unwrap(),
277        )
278    }
279
280    #[test]
281    fn test_eip3860_initcode_size_limit_failure() {
282        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE + 1];
283        let bytecode: Bytes = large_bytecode.into();
284        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
285        assert!(matches!(
286            result,
287            Err(EVMError::Transaction(
288                InvalidTransaction::CreateInitCodeSizeLimit
289            ))
290        ));
291    }
292
293    #[test]
294    fn test_eip3860_initcode_size_limit_success_prague() {
295        let large_bytecode = vec![opcode::STOP; eip3860::MAX_INITCODE_SIZE];
296        let bytecode: Bytes = large_bytecode.into();
297        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
298        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
299    }
300
301    #[test]
302    fn test_eip7907_initcode_size_limit_failure_osaka() {
303        let large_bytecode = vec![opcode::STOP; eip7907::MAX_INITCODE_SIZE + 1];
304        let bytecode: Bytes = large_bytecode.into();
305        let result = deploy_contract(bytecode, Some(SpecId::OSAKA));
306        assert!(matches!(
307            result,
308            Err(EVMError::Transaction(
309                InvalidTransaction::CreateInitCodeSizeLimit
310            ))
311        ));
312    }
313
314    #[test]
315    fn test_eip7907_code_size_limit_failure() {
316        // EIP-7907: MAX_CODE_SIZE = 0x40000
317        // use the simplest method to return a contract code size greater than 0x40000
318        // PUSH3 0x40001 (greater than 0x40000) - return size
319        // PUSH1 0x00 - memory position 0
320        // RETURN - return uninitialized memory, will be filled with 0
321        let init_code = vec![
322            0x62, 0x04, 0x00, 0x01, // PUSH3 0x40001 (greater than 0x40000)
323            0x60, 0x00, // PUSH1 0
324            0xf3, // RETURN
325        ];
326        let bytecode: Bytes = init_code.into();
327        let result = deploy_contract(bytecode, Some(SpecId::OSAKA));
328        assert!(
329            matches!(
330                result,
331                Ok(ExecutionResult::Halt {
332                    reason: HaltReason::CreateContractSizeLimit,
333                    ..
334                },)
335            ),
336            "{result:?}"
337        );
338    }
339
340    #[test]
341    fn test_eip170_code_size_limit_failure() {
342        // use the simplest method to return a contract code size greater than 0x6000
343        // PUSH3 0x6001 (greater than 0x6000) - return size
344        // PUSH1 0x00 - memory position 0
345        // RETURN - return uninitialized memory, will be filled with 0
346        let init_code = vec![
347            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (greater than 0x6000)
348            0x60, 0x00, // PUSH1 0
349            0xf3, // RETURN
350        ];
351        let bytecode: Bytes = init_code.into();
352        let result = deploy_contract(bytecode, Some(SpecId::PRAGUE));
353        assert!(
354            matches!(
355                result,
356                Ok(ExecutionResult::Halt {
357                    reason: HaltReason::CreateContractSizeLimit,
358                    ..
359                },)
360            ),
361            "{result:?}"
362        );
363    }
364
365    #[test]
366    fn test_eip170_code_size_limit_success() {
367        // use the  simplest method to return a contract code size equal to 0x6000
368        // PUSH3 0x6000 - return size
369        // PUSH1 0x00 - memory position 0
370        // RETURN - return uninitialized memory, will be filled with 0
371        let init_code = vec![
372            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000
373            0x60, 0x00, // PUSH1 0
374            0xf3, // RETURN
375        ];
376        let bytecode: Bytes = init_code.into();
377        let result = deploy_contract(bytecode, None);
378        assert!(matches!(result, Ok(ExecutionResult::Success { .. },)));
379    }
380
381    #[test]
382    fn test_eip170_create_opcode_size_limit_failure() {
383        // 1. create a "factory" contract, which will use the CREATE opcode to create another large contract
384        // 2. because the sub contract exceeds the EIP-170 limit, the CREATE operation should fail
385
386        // the bytecode of the factory contract:
387        // PUSH1 0x01      - the value for MSTORE
388        // PUSH1 0x00      - the memory position
389        // MSTORE          - store a non-zero value at the beginning of memory
390
391        // PUSH3 0x6001    - the return size (exceeds 0x6000)
392        // PUSH1 0x00      - the memory offset
393        // PUSH1 0x00      - the amount of ETH sent
394        // CREATE          - create contract instruction (create contract from current memory)
395
396        // PUSH1 0x00      - the return value storage position
397        // MSTORE          - store the address returned by CREATE to the memory position 0
398        // PUSH1 0x20      - the return size (32 bytes)
399        // PUSH1 0x00      - the return offset
400        // RETURN          - return the result
401
402        let factory_code = vec![
403            // 1. store a non-zero value at the beginning of memory
404            0x60, 0x01, // PUSH1 0x01
405            0x60, 0x00, // PUSH1 0x00
406            0x52, // MSTORE
407            // 2. prepare to create a large contract
408            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (exceeds 0x6000)
409            0x60, 0x00, // PUSH1 0x00 (the memory offset)
410            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
411            0xf0, // CREATE
412            // 3. store the address returned by CREATE to the memory position 0
413            0x60, 0x00, // PUSH1 0x00
414            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
415            // 4. return the result
416            0x60, 0x20, // PUSH1 0x20 (32 bytes)
417            0x60, 0x00, // PUSH1 0x00
418            0xf3, // RETURN
419        ];
420
421        // deploy factory contract
422        let factory_bytecode: Bytes = factory_code.into();
423        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
424            .expect("factory contract deployment failed");
425
426        // get factory contract address
427        let factory_address = match &factory_result {
428            ExecutionResult::Success {
429                output: Output::Create(_, Some(addr)),
430                ..
431            } => *addr,
432            _ => panic!("factory contract deployment failed: {factory_result:?}"),
433        };
434
435        // call factory contract to create sub contract
436        let tx_caller = address!("0x0000000000000000000000000000000000100000");
437        let call_result = Context::mainnet()
438            .with_db(CacheDB::<EmptyDB>::default())
439            .build_mainnet()
440            .transact_commit(
441                TxEnv::builder()
442                    .caller(tx_caller)
443                    .kind(TxKind::Call(factory_address))
444                    .data(Bytes::new())
445                    .build()
446                    .unwrap(),
447            )
448            .expect("call factory contract failed");
449
450        match &call_result {
451            ExecutionResult::Success { output, .. } => match output {
452                Output::Call(bytes) => {
453                    if !bytes.is_empty() {
454                        assert!(
455                            bytes.iter().all(|&b| b == 0),
456                            "When CREATE operation failed, it should return all zero address"
457                        );
458                    }
459                }
460                _ => panic!("unexpected output type"),
461            },
462            _ => panic!("execution result is not Success"),
463        }
464    }
465
466    #[test]
467    fn test_eip170_create_opcode_size_limit_success() {
468        // 1. create a "factory" contract, which will use the CREATE opcode to create another contract
469        // 2. the sub contract generated by the factory contract does not exceed the EIP-170 limit, so it should be created successfully
470
471        // the bytecode of the factory contract:
472        // PUSH1 0x01      - the value for MSTORE
473        // PUSH1 0x00      - the memory position
474        // MSTORE          - store a non-zero value at the beginning of memory
475
476        // PUSH3 0x6000    - the return size (0x6000)
477        // PUSH1 0x00      - the memory offset
478        // PUSH1 0x00      - the amount of ETH sent
479        // CREATE          - create contract instruction (create contract from current memory)
480
481        // PUSH1 0x00      - the return value storage position
482        // MSTORE          - store the address returned by CREATE to the memory position 0
483        // PUSH1 0x20      - the return size (32 bytes)
484        // PUSH1 0x00      - the return offset
485        // RETURN          - return the result
486
487        let factory_code = vec![
488            // 1. store a non-zero value at the beginning of memory
489            0x60, 0x01, // PUSH1 0x01
490            0x60, 0x00, // PUSH1 0x00
491            0x52, // MSTORE
492            // 2. prepare to create a contract
493            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000 (0x6000)
494            0x60, 0x00, // PUSH1 0x00 (the memory offset)
495            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
496            0xf0, // CREATE
497            // 3. store the address returned by CREATE to the memory position 0
498            0x60, 0x00, // PUSH1 0x00
499            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
500            // 4. return the result
501            0x60, 0x20, // PUSH1 0x20 (32 bytes)
502            0x60, 0x00, // PUSH1 0x00
503            0xf3, // RETURN
504        ];
505
506        // deploy factory contract
507        let factory_bytecode: Bytes = factory_code.into();
508        let factory_result = deploy_contract(factory_bytecode, Some(SpecId::PRAGUE))
509            .expect("factory contract deployment failed");
510        // get factory contract address
511        let factory_address = match &factory_result {
512            ExecutionResult::Success {
513                output: Output::Create(_, Some(addr)),
514                ..
515            } => *addr,
516            _ => panic!("factory contract deployment failed: {factory_result:?}"),
517        };
518
519        // call factory contract to create sub contract
520        let tx_caller = address!("0x0000000000000000000000000000000000100000");
521        let call_result = Context::mainnet()
522            .with_db(CacheDB::<EmptyDB>::default())
523            .build_mainnet()
524            .transact_commit(
525                TxEnv::builder()
526                    .caller(tx_caller)
527                    .kind(TxKind::Call(factory_address))
528                    .data(Bytes::new())
529                    .build()
530                    .unwrap(),
531            )
532            .expect("call factory contract failed");
533
534        match &call_result {
535            ExecutionResult::Success { output, .. } => {
536                match output {
537                    Output::Call(bytes) => {
538                        // check if CREATE operation is successful (return non-zero address)
539                        if !bytes.is_empty() {
540                            assert!(bytes.iter().any(|&b| b != 0), "create sub contract failed");
541                        }
542                    }
543                    _ => panic!("unexpected output type"),
544                }
545            }
546            _ => panic!("execution result is not Success"),
547        }
548    }
549}