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