revm_handler/
validation.rs

1use context_interface::{
2    journaled_state::JournalTr,
3    result::{InvalidHeader, InvalidTransaction},
4    transaction::{Transaction, TransactionType},
5    Block, Cfg, ContextTr, Database,
6};
7use core::cmp::{self, Ordering};
8use interpreter::gas::{self, InitialAndFloorGas};
9use primitives::{eip4844, hardfork::SpecId, B256, U256};
10use state::AccountInfo;
11use std::boxed::Box;
12
13pub fn validate_env<CTX: ContextTr, ERROR: From<InvalidHeader> + From<InvalidTransaction>>(
14    context: CTX,
15) -> Result<(), ERROR> {
16    let spec = context.cfg().spec().into();
17    // `prevrandao` is required for the merge
18    if spec.is_enabled_in(SpecId::MERGE) && context.block().prevrandao().is_none() {
19        return Err(InvalidHeader::PrevrandaoNotSet.into());
20    }
21    // `excess_blob_gas` is required for Cancun
22    if spec.is_enabled_in(SpecId::CANCUN) && context.block().blob_excess_gas_and_price().is_none() {
23        return Err(InvalidHeader::ExcessBlobGasNotSet.into());
24    }
25    validate_tx_env::<CTX, InvalidTransaction>(context, spec).map_err(Into::into)
26}
27
28pub fn validate_tx_against_state<
29    CTX: ContextTr,
30    ERROR: From<InvalidTransaction> + From<<CTX::Db as Database>::Error>,
31>(
32    mut context: CTX,
33) -> Result<(), ERROR> {
34    let tx_caller = context.tx().caller();
35
36    // Load acc
37    let account = context.journal().load_account_code(tx_caller)?;
38    let account = account.data.info.clone();
39
40    validate_tx_against_account(&account, context, U256::ZERO)?;
41    Ok(())
42}
43
44/// Validate transaction that has EIP-1559 priority fee
45pub fn validate_priority_fee_tx(
46    max_fee: u128,
47    max_priority_fee: u128,
48    base_fee: Option<u128>,
49) -> Result<(), InvalidTransaction> {
50    if max_priority_fee > max_fee {
51        // Or gas_max_fee for eip1559
52        return Err(InvalidTransaction::PriorityFeeGreaterThanMaxFee);
53    }
54
55    // Check minimal cost against basefee
56    if let Some(base_fee) = base_fee {
57        let effective_gas_price = cmp::min(max_fee, base_fee.saturating_add(max_priority_fee));
58        if effective_gas_price < base_fee {
59            return Err(InvalidTransaction::GasPriceLessThanBasefee);
60        }
61    }
62
63    Ok(())
64}
65
66/// Validate EIP-4844 transaction.
67pub fn validate_eip4844_tx(
68    blobs: &[B256],
69    max_blob_fee: u128,
70    block_blob_gas_price: u128,
71    max_blobs: u8,
72) -> Result<(), InvalidTransaction> {
73    // Ensure that the user was willing to at least pay the current blob gasprice
74    if block_blob_gas_price > max_blob_fee {
75        return Err(InvalidTransaction::BlobGasPriceGreaterThanMax);
76    }
77
78    // There must be at least one blob
79    if blobs.is_empty() {
80        return Err(InvalidTransaction::EmptyBlobs);
81    }
82
83    // All versioned blob hashes must start with VERSIONED_HASH_VERSION_KZG
84    for blob in blobs {
85        if blob[0] != eip4844::VERSIONED_HASH_VERSION_KZG {
86            return Err(InvalidTransaction::BlobVersionNotSupported);
87        }
88    }
89
90    // Ensure the total blob gas spent is at most equal to the limit
91    // assert blob_gas_used <= MAX_BLOB_GAS_PER_BLOCK
92    if blobs.len() > max_blobs as usize {
93        return Err(InvalidTransaction::TooManyBlobs {
94            have: blobs.len(),
95            max: max_blobs as usize,
96        });
97    }
98    Ok(())
99}
100
101/// Validate transaction against block and configuration for mainnet.
102pub fn validate_tx_env<CTX: ContextTr, Error>(
103    context: CTX,
104    spec_id: SpecId,
105) -> Result<(), InvalidTransaction> {
106    // Check if the transaction's chain id is correct
107    let tx_type = context.tx().tx_type();
108    let tx = context.tx();
109
110    let base_fee = if context.cfg().is_base_fee_check_disabled() {
111        None
112    } else {
113        Some(context.block().basefee() as u128)
114    };
115
116    match TransactionType::from(tx_type) {
117        TransactionType::Legacy => {
118            // Check chain_id only if it is present in the legacy transaction.
119            // EIP-155: Simple replay attack protection
120            if let Some(chain_id) = tx.chain_id() {
121                if chain_id != context.cfg().chain_id() {
122                    return Err(InvalidTransaction::InvalidChainId);
123                }
124            }
125            // Gas price must be at least the basefee.
126            if let Some(base_fee) = base_fee {
127                if tx.gas_price() < base_fee {
128                    return Err(InvalidTransaction::GasPriceLessThanBasefee);
129                }
130            }
131        }
132        TransactionType::Eip2930 => {
133            // Enabled in BERLIN hardfork
134            if !spec_id.is_enabled_in(SpecId::BERLIN) {
135                return Err(InvalidTransaction::Eip2930NotSupported);
136            }
137
138            if Some(context.cfg().chain_id()) != tx.chain_id() {
139                return Err(InvalidTransaction::InvalidChainId);
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
154            if Some(context.cfg().chain_id()) != tx.chain_id() {
155                return Err(InvalidTransaction::InvalidChainId);
156            }
157
158            validate_priority_fee_tx(
159                tx.max_fee_per_gas(),
160                tx.max_priority_fee_per_gas().unwrap_or_default(),
161                base_fee,
162            )?;
163        }
164        TransactionType::Eip4844 => {
165            if !spec_id.is_enabled_in(SpecId::CANCUN) {
166                return Err(InvalidTransaction::Eip4844NotSupported);
167            }
168
169            if Some(context.cfg().chain_id()) != tx.chain_id() {
170                return Err(InvalidTransaction::InvalidChainId);
171            }
172
173            validate_priority_fee_tx(
174                tx.max_fee_per_gas(),
175                tx.max_priority_fee_per_gas().unwrap_or_default(),
176                base_fee,
177            )?;
178
179            validate_eip4844_tx(
180                tx.blob_versioned_hashes(),
181                tx.max_fee_per_blob_gas(),
182                context.block().blob_gasprice().unwrap_or_default(),
183                context.cfg().blob_max_count(spec_id),
184            )?;
185        }
186        TransactionType::Eip7702 => {
187            // Check if EIP-7702 transaction is enabled.
188            if !spec_id.is_enabled_in(SpecId::PRAGUE) {
189                return Err(InvalidTransaction::Eip7702NotSupported);
190            }
191
192            if Some(context.cfg().chain_id()) != tx.chain_id() {
193                return Err(InvalidTransaction::InvalidChainId);
194            }
195
196            validate_priority_fee_tx(
197                tx.max_fee_per_gas(),
198                tx.max_priority_fee_per_gas().unwrap_or_default(),
199                base_fee,
200            )?;
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    if !context.cfg().is_block_gas_limit_disabled() && tx.gas_limit() > context.block().gas_limit()
215    {
216        return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
217    }
218
219    // EIP-3860: Limit and meter initcode
220    if spec_id.is_enabled_in(SpecId::SHANGHAI) && tx.kind().is_create() {
221        let max_initcode_size = context.cfg().max_code_size().saturating_mul(2);
222        if context.tx().input().len() > max_initcode_size {
223            return Err(InvalidTransaction::CreateInitCodeSizeLimit);
224        }
225    }
226
227    Ok(())
228}
229
230/// Validate account against the transaction.
231#[inline]
232pub fn validate_tx_against_account<CTX: ContextTr>(
233    account: &AccountInfo,
234    context: CTX,
235    additional_cost: U256,
236) -> Result<(), InvalidTransaction> {
237    let tx = context.tx();
238    let tx_type = context.tx().tx_type();
239    // EIP-3607: Reject transactions from senders with deployed code
240    // This EIP is introduced after london but there was no collision in past
241    // so we can leave it enabled always
242    if !context.cfg().is_eip3607_disabled() {
243        let bytecode = &account.code.as_ref().unwrap();
244        // Allow EOAs whose code is a valid delegation designation,
245        // i.e. 0xef0100 || address, to continue to originate transactions.
246        if !bytecode.is_empty() && !bytecode.is_eip7702() {
247            return Err(InvalidTransaction::RejectCallerWithCode);
248        }
249    }
250
251    // Check that the transaction's nonce is correct
252    if !context.cfg().is_nonce_check_disabled() {
253        let tx = tx.nonce();
254        let state = account.nonce;
255        match tx.cmp(&state) {
256            Ordering::Greater => {
257                return Err(InvalidTransaction::NonceTooHigh { tx, state });
258            }
259            Ordering::Less => {
260                return Err(InvalidTransaction::NonceTooLow { tx, state });
261            }
262            _ => {}
263        }
264    }
265
266    // gas_limit * max_fee + value + additional_gas_cost
267    let mut balance_check = U256::from(tx.gas_limit())
268        .checked_mul(U256::from(tx.max_fee_per_gas()))
269        .and_then(|gas_cost| gas_cost.checked_add(tx.value()))
270        .and_then(|gas_cost| gas_cost.checked_add(additional_cost))
271        .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;
272
273    if tx_type == TransactionType::Eip4844 {
274        let data_fee = tx.calc_max_data_fee();
275        balance_check = balance_check
276            .checked_add(data_fee)
277            .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;
278    }
279
280    // Check if account has enough balance for `gas_limit * max_fee`` and value transfer.
281    // Transfer will be done inside `*_inner` functions.
282    if balance_check > account.balance && !context.cfg().is_balance_check_disabled() {
283        return Err(InvalidTransaction::LackOfFundForMaxFee {
284            fee: Box::new(balance_check),
285            balance: Box::new(account.balance),
286        });
287    }
288
289    Ok(())
290}
291
292/// Validate initial transaction gas.
293pub fn validate_initial_tx_gas(
294    tx: impl Transaction,
295    spec: SpecId,
296) -> Result<InitialAndFloorGas, InvalidTransaction> {
297    let gas = gas::calculate_initial_tx_gas_for_tx(&tx, spec);
298
299    // Additional check to see if limit is big enough to cover initial gas.
300    if gas.initial_gas > tx.gas_limit() {
301        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
302            gas_limit: tx.gas_limit(),
303            initial_gas: gas.initial_gas,
304        });
305    }
306
307    // EIP-7623: Increase calldata cost
308    // floor gas should be less than gas limit.
309    if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas > tx.gas_limit() {
310        return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
311            gas_floor: gas.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,
326    };
327    use database::{CacheDB, EmptyDB};
328    use primitives::{address, Address, Bytes, TxKind, MAX_INITCODE_SIZE};
329
330    fn deploy_contract(
331        bytecode: Bytes,
332    ) -> Result<ExecutionResult, EVMError<core::convert::Infallible>> {
333        let ctx = Context::mainnet()
334            .modify_tx_chained(|tx| {
335                tx.kind = TxKind::Create;
336                tx.data = bytecode.clone();
337            })
338            .with_db(CacheDB::<EmptyDB>::default());
339
340        let mut evm = ctx.build_mainnet();
341        evm.replay_commit()
342    }
343
344    #[test]
345    fn test_eip3860_initcode_size_limit_failure() {
346        let large_bytecode = vec![opcode::STOP; MAX_INITCODE_SIZE + 1];
347        let bytecode: Bytes = large_bytecode.into();
348        let result = deploy_contract(bytecode);
349        assert!(matches!(
350            result,
351            Err(EVMError::Transaction(
352                InvalidTransaction::CreateInitCodeSizeLimit
353            ))
354        ));
355    }
356
357    #[test]
358    fn test_eip3860_initcode_size_limit_success() {
359        let large_bytecode = vec![opcode::STOP; MAX_INITCODE_SIZE];
360        let bytecode: Bytes = large_bytecode.into();
361        let result = deploy_contract(bytecode);
362        assert!(matches!(result, Ok(ExecutionResult::Success { .. })));
363    }
364
365    #[test]
366    fn test_eip170_code_size_limit_failure() {
367        // use the simplest method to return a contract code size greater than 0x6000
368        // PUSH3 0x6001 (greater than 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, 0x01, // PUSH3 0x6001 (greater than 0x6000)
373            0x60, 0x00, // PUSH1 0
374            0xf3, // RETURN
375        ];
376        let bytecode: Bytes = init_code.into();
377        let result = deploy_contract(bytecode);
378        assert!(matches!(
379            result,
380            Ok(ExecutionResult::Halt {
381                reason: HaltReason::CreateContractSizeLimit,
382                ..
383            },)
384        ));
385    }
386
387    #[test]
388    fn test_eip170_code_size_limit_success() {
389        // use the  simplest method to return a contract code size equal to 0x6000
390        // PUSH3 0x6000 - return size
391        // PUSH1 0x00 - memory position 0
392        // RETURN - return uninitialized memory, will be filled with 0
393        let init_code = vec![
394            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000
395            0x60, 0x00, // PUSH1 0
396            0xf3, // RETURN
397        ];
398        let bytecode: Bytes = init_code.into();
399        let result = deploy_contract(bytecode);
400        assert!(matches!(result, Ok(ExecutionResult::Success { .. },)));
401    }
402
403    #[test]
404    fn test_eip170_create_opcode_size_limit_failure() {
405        // 1. create a "factory" contract, which will use the CREATE opcode to create another large contract
406        // 2. because the sub contract exceeds the EIP-170 limit, the CREATE operation should fail
407
408        // the bytecode of the factory contract:
409        // PUSH1 0x01      - the value for MSTORE
410        // PUSH1 0x00      - the memory position
411        // MSTORE          - store a non-zero value at the beginning of memory
412
413        // PUSH3 0x6001    - the return size (exceeds 0x6000)
414        // PUSH1 0x00      - the memory offset
415        // PUSH1 0x00      - the amount of ETH sent
416        // CREATE          - create contract instruction (create contract from current memory)
417
418        // PUSH1 0x00      - the return value storage position
419        // MSTORE          - store the address returned by CREATE to the memory position 0
420        // PUSH1 0x20      - the return size (32 bytes)
421        // PUSH1 0x00      - the return offset
422        // RETURN          - return the result
423
424        let factory_code = vec![
425            // 1. store a non-zero value at the beginning of memory
426            0x60, 0x01, // PUSH1 0x01
427            0x60, 0x00, // PUSH1 0x00
428            0x52, // MSTORE
429            // 2. prepare to create a large contract
430            0x62, 0x00, 0x60, 0x01, // PUSH3 0x6001 (exceeds 0x6000)
431            0x60, 0x00, // PUSH1 0x00 (the memory offset)
432            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
433            0xf0, // CREATE
434            // 3. store the address returned by CREATE to the memory position 0
435            0x60, 0x00, // PUSH1 0x00
436            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
437            // 4. return the result
438            0x60, 0x20, // PUSH1 0x20 (32 bytes)
439            0x60, 0x00, // PUSH1 0x00
440            0xf3, // RETURN
441        ];
442
443        // deploy factory contract
444        let factory_bytecode: Bytes = factory_code.into();
445        let factory_result =
446            deploy_contract(factory_bytecode).expect("factory contract deployment failed");
447
448        // get factory contract address
449        let factory_address = match &factory_result {
450            ExecutionResult::Success { output, .. } => match output {
451                Output::Create(bytes, _) | Output::Call(bytes) => Address::from_slice(&bytes[..20]),
452            },
453            _ => panic!("factory contract deployment failed"),
454        };
455
456        // call factory contract to create sub contract
457        let tx_caller = address!("0x0000000000000000000000000000000000100000");
458        let call_result = Context::mainnet()
459            .modify_tx_chained(|tx| {
460                tx.caller = tx_caller;
461                tx.kind = TxKind::Call(factory_address);
462                tx.data = Bytes::new();
463            })
464            .with_db(CacheDB::<EmptyDB>::default())
465            .build_mainnet()
466            .replay_commit()
467            .expect("call factory contract failed");
468
469        match &call_result {
470            ExecutionResult::Success { output, .. } => match output {
471                Output::Call(bytes) => {
472                    if !bytes.is_empty() {
473                        assert!(
474                            bytes.iter().all(|&b| b == 0),
475                            "When CREATE operation failed, it should return all zero address"
476                        );
477                    }
478                }
479                _ => panic!("unexpected output type"),
480            },
481            _ => panic!("execution result is not Success"),
482        }
483    }
484
485    #[test]
486    fn test_eip170_create_opcode_size_limit_success() {
487        // 1. create a "factory" contract, which will use the CREATE opcode to create another contract
488        // 2. the sub contract generated by the factory contract does not exceed the EIP-170 limit, so it should be created successfully
489
490        // the bytecode of the factory contract:
491        // PUSH1 0x01      - the value for MSTORE
492        // PUSH1 0x00      - the memory position
493        // MSTORE          - store a non-zero value at the beginning of memory
494
495        // PUSH3 0x6000    - the return size (0x6000)
496        // PUSH1 0x00      - the memory offset
497        // PUSH1 0x00      - the amount of ETH sent
498        // CREATE          - create contract instruction (create contract from current memory)
499
500        // PUSH1 0x00      - the return value storage position
501        // MSTORE          - store the address returned by CREATE to the memory position 0
502        // PUSH1 0x20      - the return size (32 bytes)
503        // PUSH1 0x00      - the return offset
504        // RETURN          - return the result
505
506        let factory_code = vec![
507            // 1. store a non-zero value at the beginning of memory
508            0x60, 0x01, // PUSH1 0x01
509            0x60, 0x00, // PUSH1 0x00
510            0x52, // MSTORE
511            // 2. prepare to create a contract
512            0x62, 0x00, 0x60, 0x00, // PUSH3 0x6000 (0x6000)
513            0x60, 0x00, // PUSH1 0x00 (the memory offset)
514            0x60, 0x00, // PUSH1 0x00 (the amount of ETH sent)
515            0xf0, // CREATE
516            // 3. store the address returned by CREATE to the memory position 0
517            0x60, 0x00, // PUSH1 0x00
518            0x52, // MSTORE (store the address returned by CREATE to the memory position 0)
519            // 4. return the result
520            0x60, 0x20, // PUSH1 0x20 (32 bytes)
521            0x60, 0x00, // PUSH1 0x00
522            0xf3, // RETURN
523        ];
524
525        // deploy factory contract
526        let factory_bytecode: Bytes = factory_code.into();
527        let factory_result =
528            deploy_contract(factory_bytecode).expect("factory contract deployment failed");
529        // get factory contract address
530        let factory_address = match &factory_result {
531            ExecutionResult::Success { output, .. } => match output {
532                Output::Create(bytes, _) | Output::Call(bytes) => Address::from_slice(&bytes[..20]),
533            },
534            _ => panic!("factory contract deployment failed"),
535        };
536
537        // call factory contract to create sub contract
538        let tx_caller = address!("0x0000000000000000000000000000000000100000");
539        let call_result = Context::mainnet()
540            .modify_tx_chained(|tx| {
541                tx.caller = tx_caller;
542                tx.kind = TxKind::Call(factory_address);
543                tx.data = Bytes::new();
544            })
545            .with_db(CacheDB::<EmptyDB>::default())
546            .build_mainnet()
547            .replay_commit()
548            .expect("call factory contract failed");
549
550        match &call_result {
551            ExecutionResult::Success { output, .. } => {
552                match output {
553                    Output::Call(bytes) => {
554                        // check if CREATE operation is successful (return non-zero address)
555                        if !bytes.is_empty() {
556                            assert!(bytes.iter().any(|&b| b != 0), "create sub contract failed");
557                        }
558                    }
559                    _ => panic!("unexpected output type"),
560                }
561            }
562            _ => panic!("execution result is not Success"),
563        }
564    }
565}