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}