diff --git a/Cargo.lock b/Cargo.lock index 9dc7aeb3f4..bccd7d130d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1982,9 +1982,8 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "evm" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1099df1dac16f32a136452ad98ee0f1ff42acd3e12ce65bea4462b61d656608a" +version = "0.39.1" +source = "git+https://github.com/rust-blockchain/evm?branch=master#e85c34f96e3237c09955193b41154030b78119c5" dependencies = [ "auto_impl", "environmental", @@ -2004,8 +2003,7 @@ dependencies = [ [[package]] name = "evm-core" version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1f13264b044cb66f0602180f0bc781c29accb41ff560669a3ec15858d5b606" +source = "git+https://github.com/rust-blockchain/evm?branch=master#e85c34f96e3237c09955193b41154030b78119c5" dependencies = [ "parity-scale-codec", "primitive-types", @@ -2016,8 +2014,7 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d43eadc395bd1a52990787ca1495c26b0248165444912be075c28909a853b8c" +source = "git+https://github.com/rust-blockchain/evm?branch=master#e85c34f96e3237c09955193b41154030b78119c5" dependencies = [ "environmental", "evm-core", @@ -2028,8 +2025,7 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa5b32f59ec582a5651978004e5c784920291263b7dcb6de418047438e37f4f" +source = "git+https://github.com/rust-blockchain/evm?branch=master#e85c34f96e3237c09955193b41154030b78119c5" dependencies = [ "auto_impl", "environmental", @@ -10000,7 +9996,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.6", - "rand 0.8.5", + "rand 0.7.3", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 33c27f6102..e71fc735cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ bn = { package = "substrate-bn", version = "0.6", default-features = false } environmental = { version = "1.1.4", default-features = false } ethereum = { version = "0.14.0", default-features = false } ethereum-types = { version = "0.14.1", default-features = false } -evm = { version = "0.39.0", default-features = false } +evm = { git = "https://github.com/rust-blockchain/evm", branch = "master", default-features = false } hex-literal = { version = "0.3.4" } impl-serde = { version = "0.4.0", default-features = false } jsonrpsee = "0.16.2" diff --git a/client/rpc/src/eth/execute.rs b/client/rpc/src/eth/execute.rs index d603043b9b..00bd6656b2 100644 --- a/client/rpc/src/eth/execute.rs +++ b/client/rpc/src/eth/execute.rs @@ -34,7 +34,7 @@ use sp_runtime::{traits::Block as BlockT, DispatchError, SaturatedConversion}; use sp_state_machine::OverlayedChanges; // Frontier use fc_rpc_core::types::*; -use fp_evm::CallInfo; +use fp_evm::{ExecutionInfo, ExecutionInfoV2}; use fp_rpc::{EthereumRuntimeRPCApi, RuntimeStorageOverride}; use fp_storage::{EVM_ACCOUNT_CODES, PALLET_EVM}; @@ -210,7 +210,7 @@ where error_on_execution_failure(&info.exit_reason, &info.value)?; Ok(Bytes(info.value)) - } else if api_version == 4 { + } else if api_version == 4 || api_version == 5 { // Post-london + access list support let encoded_params = Encode::encode(&( &from.unwrap_or_default(), @@ -247,23 +247,47 @@ where recorder: &None, }; - let info = self - .client - .call_api_at(params) - .and_then(|r| { - Result::map_err( - as Decode>::decode(&mut &r[..]), - |error| sp_api::ApiError::FailedToDecodeReturnValue { - function: "EthereumRuntimeRPCApi_call", - error, - }, - ) - }) - .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + let value = if api_version == 4 { + let info = self + .client + .call_api_at(params) + .and_then(|r| { + Result::map_err( + >, DispatchError> as Decode>::decode(&mut &r[..]), + |error| sp_api::ApiError::FailedToDecodeReturnValue { + function: "EthereumRuntimeRPCApi_call", + error, + }, + ) + }) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; - error_on_execution_failure(&info.exit_reason, &info.value)?; - Ok(Bytes(info.value)) + error_on_execution_failure(&info.exit_reason, &info.value)?; + info.value + } else if api_version == 5 { + let info = self + .client + .call_api_at(params) + .and_then(|r| { + Result::map_err( + >, DispatchError> as Decode>::decode(&mut &r[..]), + |error| sp_api::ApiError::FailedToDecodeReturnValue { + function: "EthereumRuntimeRPCApi_call", + error, + }, + ) + }) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + error_on_execution_failure(&info.exit_reason, &info.value)?; + info.value + } else { + unreachable!("invalid version"); + }; + + Ok(Bytes(value)) } else { Err(internal_err("failed to retrieve Runtime Api version")) } @@ -315,6 +339,36 @@ where .map_err(|err| internal_err(format!("runtime error: {:?}", err)))?; Ok(Bytes(code)) } else if api_version == 4 { + // Post-london + access list support + let access_list = access_list.unwrap_or_default(); + #[allow(deprecated)] + let info = api.create_before_version_5( + substrate_hash, + from.unwrap_or_default(), + data, + value.unwrap_or_default(), + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + false, + Some( + access_list + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect(), + ), + ) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + error_on_execution_failure(&info.exit_reason, &[])?; + + let code = api + .account_code_at(substrate_hash, info.value) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))?; + Ok(Bytes(code)) + } else if api_version == 5 { // Post-london + access list support let access_list = access_list.unwrap_or_default(); let info = api @@ -515,10 +569,10 @@ where let (exit_reason, data, used_gas) = match to { Some(to) => { - let info = if api_version == 1 { + if api_version == 1 { // Legacy pre-london #[allow(deprecated)] - api.call_before_version_2( + let info = api.call_before_version_2( substrate_hash, from.unwrap_or_default(), to, @@ -530,11 +584,33 @@ where estimate_mode, ) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + (info.exit_reason, info.value, info.used_gas) } else if api_version < 4 { // Post-london #[allow(deprecated)] - api.call_before_version_4( + let info = api.call_before_version_4( + substrate_hash, + from.unwrap_or_default(), + to, + data, + value.unwrap_or_default(), + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + estimate_mode, + ) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + (info.exit_reason, info.value, info.used_gas) + } else if api_version == 4 { + // Post-london + access list support + let access_list = access_list.unwrap_or_default(); + #[allow(deprecated)] + let info = api.call_before_version_5( substrate_hash, from.unwrap_or_default(), to, @@ -545,13 +621,21 @@ where max_priority_fee_per_gas, nonce, estimate_mode, + Some( + access_list + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect(), + ), ) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + (info.exit_reason, info.value, info.used_gas) } else { // Post-london + access list support let access_list = access_list.unwrap_or_default(); - api.call( + let info = api.call( substrate_hash, from.unwrap_or_default(), to, @@ -570,16 +654,16 @@ where ), ) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))? - }; + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; - (info.exit_reason, info.value, info.used_gas) + (info.exit_reason, info.value, info.used_gas.effective) + } } None => { - let info = if api_version == 1 { + if api_version == 1 { // Legacy pre-london #[allow(deprecated)] - api.create_before_version_2( + let info = api.create_before_version_2( substrate_hash, from.unwrap_or_default(), data, @@ -590,11 +674,13 @@ where estimate_mode, ) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + (info.exit_reason, Vec::new(), info.used_gas) } else if api_version < 4 { // Post-london #[allow(deprecated)] - api.create_before_version_4( + let info = api.create_before_version_4( substrate_hash, from.unwrap_or_default(), data, @@ -606,11 +692,38 @@ where estimate_mode, ) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + (info.exit_reason, Vec::new(), info.used_gas) + } else if api_version == 4 { + // Post-london + access list support + let access_list = access_list.unwrap_or_default(); + #[allow(deprecated)] + let info = api.create_before_version_5( + substrate_hash, + from.unwrap_or_default(), + data, + value.unwrap_or_default(), + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + nonce, + estimate_mode, + Some( + access_list + .into_iter() + .map(|item| (item.address, item.storage_keys)) + .collect(), + ), + ) + .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; + + (info.exit_reason, Vec::new(), info.used_gas) } else { // Post-london + access list support let access_list = access_list.unwrap_or_default(); - api.create( + let info = api.create( substrate_hash, from.unwrap_or_default(), data, @@ -628,10 +741,10 @@ where ), ) .map_err(|err| internal_err(format!("runtime error: {:?}", err)))? - .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))? - }; + .map_err(|err| internal_err(format!("execution fatal: {:?}", err)))?; - (info.exit_reason, Vec::new(), info.used_gas) + (info.exit_reason, Vec::new(), info.used_gas.effective) + } } }; Ok(ExecutableResult { diff --git a/frame/ethereum/src/lib.rs b/frame/ethereum/src/lib.rs index 975a5d46f3..d9f1131db8 100644 --- a/frame/ethereum/src/lib.rs +++ b/frame/ethereum/src/lib.rs @@ -353,6 +353,16 @@ pub mod pallet { } impl Pallet { + fn transaction_len(transaction: &Transaction) -> u64 { + transaction + .encode() + .len() + // pallet index + .saturating_add(1) + // call index + .saturating_add(1) as u64 + } + fn recover_signer(transaction: &Transaction) -> Option { let mut sig = [0u8; 65]; let mut msg = [0u8; 32]; @@ -473,6 +483,17 @@ impl Pallet { let transaction_data: TransactionData = transaction.into(); let transaction_nonce = transaction_data.nonce; + let (weight_limit, proof_size_base_cost) = + match ::GasWeightMapping::gas_to_weight( + transaction_data.gas_limit.unique_saturated_into(), + true, + ) { + weight_limit if weight_limit.proof_size() > 0 => { + (Some(weight_limit), Some(Self::transaction_len(transaction))) + } + _ => (None, None), + }; + let (base_fee, _) = T::FeeCalculator::min_gas_price(); let (who, _) = pallet_evm::Pallet::::account_basic(&origin); @@ -485,6 +506,8 @@ impl Pallet { is_transactional: true, }, transaction_data.clone().into(), + weight_limit, + proof_size_base_cost, ) .validate_in_pool_for(&who) .and_then(|v| v.with_chain_id()) @@ -540,7 +563,7 @@ impl Pallet { let transaction_hash = transaction.hash(); let transaction_index = pending.len() as u32; - let (reason, status, used_gas, dest, extra_data) = match info { + let (reason, status, weight_info, used_gas, dest, extra_data) = match info { CallOrCreateInfo::Call(info) => ( info.exit_reason.clone(), TransactionStatus { @@ -556,6 +579,7 @@ impl Pallet { bloom }, }, + info.weight_info, info.used_gas, to, match info.exit_reason { @@ -599,6 +623,7 @@ impl Pallet { bloom }, }, + info.weight_info, info.used_gas, Some(info.value), Vec::new(), @@ -615,11 +640,11 @@ impl Pallet { let cumulative_gas_used = if let Some((_, _, receipt)) = pending.last() { match receipt { Receipt::Legacy(d) | Receipt::EIP2930(d) | Receipt::EIP1559(d) => { - d.used_gas.saturating_add(used_gas) + d.used_gas.saturating_add(used_gas.effective) } } } else { - used_gas + used_gas.effective }; match &transaction { Transaction::Legacy(_) => Receipt::Legacy(ethereum::EIP658ReceiptData { @@ -654,10 +679,18 @@ impl Pallet { }); Ok(PostDispatchInfo { - actual_weight: Some(T::GasWeightMapping::gas_to_weight( - used_gas.unique_saturated_into(), - true, - )), + actual_weight: { + let mut gas_to_weight = T::GasWeightMapping::gas_to_weight( + used_gas.standard.unique_saturated_into(), + true, + ); + if let Some(weight_info) = weight_info { + if let Some(proof_size_usage) = weight_info.proof_size_usage { + *gas_to_weight.proof_size_mut() = proof_size_usage; + } + } + Some(gas_to_weight) + }, pays_fee: Pays::No, }) } @@ -738,6 +771,17 @@ impl Pallet { let is_transactional = true; let validate = false; + + let (transaction_len, weight_limit) = + match ::GasWeightMapping::gas_to_weight( + gas_limit.unique_saturated_into(), + true, + ) { + weight_limit if weight_limit.proof_size() > 0 => { + (Some(Self::transaction_len(transaction)), Some(weight_limit)) + } + _ => (None, None), + }; match action { ethereum::TransactionAction::Call(target) => { let res = match T::Runner::call( @@ -752,6 +796,8 @@ impl Pallet { access_list, is_transactional, validate, + weight_limit, + transaction_len, config.as_ref().unwrap_or_else(|| T::config()), ) { Ok(res) => res, @@ -780,6 +826,8 @@ impl Pallet { access_list, is_transactional, validate, + weight_limit, + transaction_len, config.as_ref().unwrap_or_else(|| T::config()), ) { Ok(res) => res, @@ -812,6 +860,17 @@ impl Pallet { let (base_fee, _) = T::FeeCalculator::min_gas_price(); let (who, _) = pallet_evm::Pallet::::account_basic(&origin); + let (weight_limit, proof_size_base_cost) = + match ::GasWeightMapping::gas_to_weight( + transaction_data.gas_limit.unique_saturated_into(), + true, + ) { + weight_limit if weight_limit.proof_size() > 0 => { + (Some(weight_limit), Some(Self::transaction_len(transaction))) + } + _ => (None, None), + }; + let _ = CheckEvmTransaction::::new( CheckEvmTransactionConfig { evm_config: T::config(), @@ -821,6 +880,8 @@ impl Pallet { is_transactional: true, }, transaction_data.into(), + weight_limit, + proof_size_base_cost, ) .validate_in_block_for(&who) .and_then(|v| v.with_chain_id()) diff --git a/frame/ethereum/src/mock.rs b/frame/ethereum/src/mock.rs index 90eb823d65..237f6de0ae 100644 --- a/frame/ethereum/src/mock.rs +++ b/frame/ethereum/src/mock.rs @@ -134,11 +134,15 @@ impl FindAuthor for FindAuthorTruncated { } } +const BLOCK_GAS_LIMIT: u64 = 150_000_000; +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + parameter_types! { pub const TransactionByteFee: u64 = 1; pub const ChainId: u64 = 42; pub const EVMModuleId: PalletId = PalletId(*b"py/evmpa"); - pub const BlockGasLimit: U256 = U256::MAX; + pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); + pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); pub const WeightPerGas: Weight = Weight::from_parts(20_000, 0); } @@ -169,6 +173,7 @@ impl pallet_evm::Config for Test { type OnChargeTransaction = (); type OnCreate = (); type FindAuthor = FindAuthorTruncated; + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type Timestamp = Timestamp; type WeightInfo = (); } diff --git a/frame/ethereum/src/tests/eip1559.rs b/frame/ethereum/src/tests/eip1559.rs index 8a55e33033..d0091d6ab0 100644 --- a/frame/ethereum/src/tests/eip1559.rs +++ b/frame/ethereum/src/tests/eip1559.rs @@ -335,7 +335,7 @@ fn transaction_should_generate_correct_gas_used() { match info { CallOrCreateInfo::Create(info) => { - assert_eq!(info.used_gas, expected_gas); + assert_eq!(info.used_gas.standard, expected_gas); } CallOrCreateInfo::Call(_) => panic!("expected create info"), } @@ -422,7 +422,7 @@ fn event_extra_data_should_be_handle_properly() { input: hex::decode(TEST_CONTRACT_CODE).unwrap(), } .sign(&alice.private_key, None); - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t,)); let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap(); let foo = hex::decode("c2985578").unwrap(); @@ -440,7 +440,7 @@ fn event_extra_data_should_be_handle_properly() { .sign(&alice.private_key, None); // calling foo - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t2)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t2,)); System::assert_last_event(RuntimeEvent::Ethereum(Event::Executed { from: alice.address, to: H160::from_slice(&contract_address), @@ -464,7 +464,7 @@ fn event_extra_data_should_be_handle_properly() { .sign(&alice.private_key, None); // calling bar revert - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t3)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t3,)); System::assert_last_event(RuntimeEvent::Ethereum(Event::Executed { from: alice.address, to: H160::from_slice(&contract_address), @@ -553,3 +553,36 @@ fn validated_transaction_apply_zero_gas_price_works() { assert_eq!(Balances::free_balance(&substrate_bob), 1_100); }); } + +#[test] +fn proof_size_weight_limit_validation_works() { + let (pairs, mut ext) = new_test_ext(1); + let alice = &pairs[0]; + + ext.execute_with(|| { + let mut tx = EIP1559UnsignedTransaction { + nonce: U256::from(2), + max_priority_fee_per_gas: U256::zero(), + max_fee_per_gas: U256::from(1), + gas_limit: U256::from(0x100000), + action: ethereum::TransactionAction::Call(alice.address), + value: U256::from(1), + input: Vec::new(), + }; + + let gas_limit: u64 = 1_000_000; + tx.gas_limit = U256::from(gas_limit); + + let weight_limit = + ::GasWeightMapping::gas_to_weight(gas_limit, true); + + // Gas limit cannot afford the extra byte and thus is expected to exhaust. + tx.input = vec![0u8; (weight_limit.proof_size() + 1) as usize]; + let tx = tx.sign(&alice.private_key, None); + + // Execute + assert!( + Ethereum::transact(RawOrigin::EthereumTransaction(alice.address).into(), tx,).is_err() + ); + }); +} diff --git a/frame/ethereum/src/tests/eip2930.rs b/frame/ethereum/src/tests/eip2930.rs index 56543f62dc..6b3a6cd34d 100644 --- a/frame/ethereum/src/tests/eip2930.rs +++ b/frame/ethereum/src/tests/eip2930.rs @@ -267,7 +267,7 @@ fn transaction_should_generate_correct_gas_used() { match info { CallOrCreateInfo::Create(info) => { - assert_eq!(info.used_gas, expected_gas); + assert_eq!(info.used_gas.standard, expected_gas); } CallOrCreateInfo::Call(_) => panic!("expected create info"), } @@ -350,7 +350,7 @@ fn event_extra_data_should_be_handle_properly() { input: hex::decode(TEST_CONTRACT_CODE).unwrap(), } .sign(&alice.private_key, None); - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t,)); let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap(); let foo = hex::decode("c2985578").unwrap(); @@ -367,7 +367,7 @@ fn event_extra_data_should_be_handle_properly() { .sign(&alice.private_key, None); // calling foo - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t2)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t2,)); System::assert_last_event(RuntimeEvent::Ethereum(Event::Executed { from: alice.address, to: H160::from_slice(&contract_address), @@ -390,7 +390,7 @@ fn event_extra_data_should_be_handle_properly() { .sign(&alice.private_key, None); // calling bar revert - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t3)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t3,)); System::assert_last_event(RuntimeEvent::Ethereum(Event::Executed { from: alice.address, to: H160::from_slice(&contract_address), @@ -478,3 +478,37 @@ fn validated_transaction_apply_zero_gas_price_works() { assert_eq!(Balances::free_balance(&substrate_bob), 1_100); }); } + +#[test] +fn proof_size_weight_limit_validation_works() { + use pallet_evm::GasWeightMapping; + + let (pairs, mut ext) = new_test_ext(1); + let alice = &pairs[0]; + + ext.execute_with(|| { + let mut tx = EIP2930UnsignedTransaction { + nonce: U256::from(2), + gas_price: U256::from(1), + gas_limit: U256::from(0x100000), + action: ethereum::TransactionAction::Call(alice.address), + value: U256::from(1), + input: Vec::new(), + }; + + let gas_limit: u64 = 1_000_000; + tx.gas_limit = U256::from(gas_limit); + + let weight_limit = + ::GasWeightMapping::gas_to_weight(gas_limit, true); + + // Gas limit cannot afford the extra byte and thus is expected to exhaust. + tx.input = vec![0u8; (weight_limit.proof_size() + 1) as usize]; + let tx = tx.sign(&alice.private_key, None); + + // Execute + assert!( + Ethereum::transact(RawOrigin::EthereumTransaction(alice.address).into(), tx,).is_err() + ); + }); +} diff --git a/frame/ethereum/src/tests/legacy.rs b/frame/ethereum/src/tests/legacy.rs index 777ff1bb88..c119da3700 100644 --- a/frame/ethereum/src/tests/legacy.rs +++ b/frame/ethereum/src/tests/legacy.rs @@ -267,7 +267,7 @@ fn transaction_should_generate_correct_gas_used() { match info { CallOrCreateInfo::Create(info) => { - assert_eq!(info.used_gas, expected_gas); + assert_eq!(info.used_gas.standard, expected_gas); } CallOrCreateInfo::Call(_) => panic!("expected create info"), } @@ -367,7 +367,7 @@ fn event_extra_data_should_be_handle_properly() { .sign(&alice.private_key); // calling foo - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t2)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t2,)); System::assert_last_event(RuntimeEvent::Ethereum(Event::Executed { from: alice.address, to: H160::from_slice(&contract_address), @@ -390,7 +390,7 @@ fn event_extra_data_should_be_handle_properly() { .sign(&alice.private_key); // calling bar revert - assert_ok!(Ethereum::apply_validated_transaction(alice.address, t3)); + assert_ok!(Ethereum::apply_validated_transaction(alice.address, t3,)); System::assert_last_event(RuntimeEvent::Ethereum(Event::Executed { from: alice.address, to: H160::from_slice(&contract_address), @@ -478,3 +478,37 @@ fn validated_transaction_apply_zero_gas_price_works() { assert_eq!(Balances::free_balance(&substrate_bob), 1_100); }); } + +#[test] +fn proof_size_weight_limit_validation_works() { + use pallet_evm::GasWeightMapping; + + let (pairs, mut ext) = new_test_ext(1); + let alice = &pairs[0]; + + ext.execute_with(|| { + let mut tx = LegacyUnsignedTransaction { + nonce: U256::from(2), + gas_price: U256::from(1), + gas_limit: U256::from(0x100000), + action: ethereum::TransactionAction::Call(alice.address), + value: U256::from(1), + input: Vec::new(), + }; + + let gas_limit: u64 = 1_000_000; + tx.gas_limit = U256::from(gas_limit); + + let weight_limit = + ::GasWeightMapping::gas_to_weight(gas_limit, true); + + // Gas limit cannot afford the extra byte and thus is expected to exhaust. + tx.input = vec![0u8; (weight_limit.proof_size() + 1) as usize]; + let tx = tx.sign(&alice.private_key); + + // Execute + assert!( + Ethereum::transact(RawOrigin::EthereumTransaction(alice.address).into(), tx,).is_err() + ); + }); +} diff --git a/frame/evm/precompile/dispatch/src/lib.rs b/frame/evm/precompile/dispatch/src/lib.rs index 6a4d3d46fd..f73349bf31 100644 --- a/frame/evm/precompile/dispatch/src/lib.rs +++ b/frame/evm/precompile/dispatch/src/lib.rs @@ -81,14 +81,28 @@ where return Err(err); } + handle + .record_external_cost(Some(info.weight.ref_time()), Some(info.weight.proof_size()))?; + match call.dispatch(Some(origin).into()) { Ok(post_info) => { if post_info.pays_fee(&info) == Pays::Yes { - let cost = T::GasWeightMapping::weight_to_gas( - post_info.actual_weight.unwrap_or(info.weight), - ); - + let actual_weight = post_info.actual_weight.unwrap_or(info.weight); + let cost = T::GasWeightMapping::weight_to_gas(actual_weight); handle.record_cost(cost)?; + + handle.refund_external_cost( + Some( + info.weight + .ref_time() + .saturating_sub(actual_weight.ref_time()), + ), + Some( + info.weight + .proof_size() + .saturating_sub(actual_weight.proof_size()), + ), + ); } Ok(PrecompileOutput { diff --git a/frame/evm/precompile/dispatch/src/mock.rs b/frame/evm/precompile/dispatch/src/mock.rs index 7f0acfe231..e08be3ff22 100644 --- a/frame/evm/precompile/dispatch/src/mock.rs +++ b/frame/evm/precompile/dispatch/src/mock.rs @@ -159,6 +159,7 @@ impl pallet_evm::Config for Test { type OnChargeTransaction = (); type OnCreate = (); type FindAuthor = FindAuthorTruncated; + type GasLimitPovSizeRatio = (); type Timestamp = Timestamp; type WeightInfo = (); } @@ -185,6 +186,16 @@ impl PrecompileHandle for MockHandle { Ok(()) } + fn record_external_cost( + &mut self, + _ref_time: Option, + _proof_size: Option, + ) -> Result<(), ExitError> { + Ok(()) + } + + fn refund_external_cost(&mut self, _ref_time: Option, _proof_size: Option) {} + fn remaining_gas(&self) -> u64 { unimplemented!() } diff --git a/frame/evm/src/benchmarking.rs b/frame/evm/src/benchmarking.rs index 97b885d22e..bf65792f7f 100644 --- a/frame/evm/src/benchmarking.rs +++ b/frame/evm/src/benchmarking.rs @@ -89,6 +89,8 @@ benchmarks! { Vec::new(), is_transactional, validate, + None, + None, T::config(), ); assert!(create_runner_results.is_ok(), "create() failed"); @@ -124,6 +126,8 @@ benchmarks! { Vec::new(), is_transactional, validate, + None, + None, T::config(), ); assert!(call_runner_results.is_ok(), "call() failed"); diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index ca84889b1e..b96eaca1ed 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -67,7 +67,7 @@ mod tests; pub mod weights; use frame_support::{ - dispatch::{DispatchResultWithPostInfo, Pays, PostDispatchInfo}, + dispatch::{DispatchResultWithPostInfo, MaxEncodedLen, Pays, PostDispatchInfo}, traits::{ tokens::fungible::Inspect, Currency, ExistenceRequirement, FindAuthor, Get, Imbalance, OnUnbalanced, SignedImbalance, Time, WithdrawReasons, @@ -91,9 +91,10 @@ use fp_account::AccountId20; #[cfg(feature = "std")] use fp_evm::GenesisAccount; pub use fp_evm::{ - Account, CallInfo, CreateInfo, ExecutionInfo, FeeCalculator, InvalidEvmTransactionError, - IsPrecompileResult, LinearCostPrecompile, Log, Precompile, PrecompileFailure, PrecompileHandle, - PrecompileOutput, PrecompileResult, PrecompileSet, Vicinity, + Account, CallInfo, CreateInfo, ExecutionInfoV2 as ExecutionInfo, FeeCalculator, + InvalidEvmTransactionError, IsPrecompileResult, LinearCostPrecompile, Log, Precompile, + PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, PrecompileSet, + Vicinity, }; pub use self::{ @@ -159,6 +160,9 @@ pub mod pallet { /// Find author for the current block. type FindAuthor: FindAuthor; + /// Gas limit Pov size ratio. + type GasLimitPovSizeRatio: Get; + /// Get the timestamp for the current block. type Timestamp: Time; @@ -228,6 +232,8 @@ pub mod pallet { access_list, is_transactional, validate, + None, + None, T::config(), ) { Ok(info) => info, @@ -252,10 +258,18 @@ pub mod pallet { }; Ok(PostDispatchInfo { - actual_weight: Some(T::GasWeightMapping::gas_to_weight( - info.used_gas.unique_saturated_into(), - true, - )), + actual_weight: { + let mut gas_to_weight = T::GasWeightMapping::gas_to_weight( + info.used_gas.standard.unique_saturated_into(), + true, + ); + if let Some(weight_info) = info.weight_info { + if let Some(proof_size_usage) = weight_info.proof_size_usage { + *gas_to_weight.proof_size_mut() = proof_size_usage; + } + } + Some(gas_to_weight) + }, pays_fee: Pays::No, }) } @@ -293,6 +307,8 @@ pub mod pallet { access_list, is_transactional, validate, + None, + None, T::config(), ) { Ok(info) => info, @@ -329,10 +345,18 @@ pub mod pallet { } Ok(PostDispatchInfo { - actual_weight: Some(T::GasWeightMapping::gas_to_weight( - info.used_gas.unique_saturated_into(), - true, - )), + actual_weight: { + let mut gas_to_weight = T::GasWeightMapping::gas_to_weight( + info.used_gas.standard.unique_saturated_into(), + true, + ); + if let Some(weight_info) = info.weight_info { + if let Some(proof_size_usage) = weight_info.proof_size_usage { + *gas_to_weight.proof_size_mut() = proof_size_usage; + } + } + Some(gas_to_weight) + }, pays_fee: Pays::No, }) } @@ -371,6 +395,8 @@ pub mod pallet { access_list, is_transactional, validate, + None, + None, T::config(), ) { Ok(info) => info, @@ -407,10 +433,18 @@ pub mod pallet { } Ok(PostDispatchInfo { - actual_weight: Some(T::GasWeightMapping::gas_to_weight( - info.used_gas.unique_saturated_into(), - true, - )), + actual_weight: { + let mut gas_to_weight = T::GasWeightMapping::gas_to_weight( + info.used_gas.standard.unique_saturated_into(), + true, + ); + if let Some(weight_info) = info.weight_info { + if let Some(proof_size_usage) = weight_info.proof_size_usage { + *gas_to_weight.proof_size_mut() = proof_size_usage; + } + } + Some(gas_to_weight) + }, pays_fee: Pays::No, }) } @@ -530,7 +564,17 @@ pub type BalanceOf = type NegativeImbalanceOf = ::AccountId>>::NegativeImbalance; -#[derive(Debug, Clone, Copy, Eq, PartialEq, Encode, Decode, TypeInfo)] +#[derive( + Debug, + Clone, + Copy, + Eq, + PartialEq, + Encode, + Decode, + TypeInfo, + MaxEncodedLen +)] pub struct CodeMetadata { pub size: u64, pub hash: H256, @@ -707,6 +751,13 @@ impl GasWeightMapping for FixedGasWeightMapping { .base_extrinsic, ); } + // Apply a gas to proof size ratio based on BlockGasLimit + let ratio = T::GasLimitPovSizeRatio::get(); + if ratio > 0 { + let proof_size = gas.saturating_div(ratio); + *weight.proof_size_mut() = proof_size; + } + weight } fn weight_to_gas(weight: Weight) -> u64 { diff --git a/frame/evm/src/mock.rs b/frame/evm/src/mock.rs index 10df7514b2..88d050a6ce 100644 --- a/frame/evm/src/mock.rs +++ b/frame/evm/src/mock.rs @@ -17,7 +17,6 @@ //! Test mock for unit tests and benchmarking -use fp_evm::{IsPrecompileResult, Precompile}; use frame_support::{ parameter_types, traits::{ConstU32, FindAuthor}, @@ -32,8 +31,8 @@ use sp_runtime::{ use sp_std::{boxed::Box, prelude::*, str::FromStr}; use crate::{ - EnsureAddressNever, EnsureAddressRoot, FeeCalculator, IdentityAddressMapping, PrecompileHandle, - PrecompileResult, PrecompileSet, + EnsureAddressNever, EnsureAddressRoot, FeeCalculator, IdentityAddressMapping, + IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, PrecompileSet, }; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -126,8 +125,12 @@ impl FindAuthor for FindAuthorTruncated { Some(H160::from_str("1234500000000000000000000000000000000000").unwrap()) } } +const BLOCK_GAS_LIMIT: u64 = 150_000_000; +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + parameter_types! { - pub BlockGasLimit: U256 = U256::max_value(); + pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); + pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); pub WeightPerGas: Weight = Weight::from_parts(20_000, 0); pub MockPrecompiles: MockPrecompileSet = MockPrecompileSet; } @@ -152,6 +155,7 @@ impl crate::Config for Test { type OnChargeTransaction = (); type OnCreate = (); type FindAuthor = FindAuthorTruncated; + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type Timestamp = Timestamp; type WeightInfo = (); } diff --git a/frame/evm/src/res/proof_size_test_callee_contract_bytecode.txt b/frame/evm/src/res/proof_size_test_callee_contract_bytecode.txt new file mode 100644 index 0000000000..40f0a191bc --- /dev/null +++ b/frame/evm/src/res/proof_size_test_callee_contract_bytecode.txt @@ -0,0 +1 @@ +6080604052348015600f57600080fd5b50607480601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ac4c25b214602d575b600080fd5b60336035565b005b6000600190505056fea2646970667358221220eae4df57558ab19ae5b916be34b7789b8e52d806b4680224965e76ab9554d77d64736f6c63430008120033 \ No newline at end of file diff --git a/frame/evm/src/res/proof_size_test_contract_bytecode.txt b/frame/evm/src/res/proof_size_test_contract_bytecode.txt new file mode 100644 index 0000000000..006b2d018e --- /dev/null +++ b/frame/evm/src/res/proof_size_test_contract_bytecode.txt @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506006600081905550610410806100286000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c806335f56c3b1461005c5780634f3080a914610078578063944ddc6214610082578063c6d6f6061461008c578063e27a0ecd146100a8575b600080fd5b61007660048036038101906100719190610265565b6100c6565b005b610080610103565b005b61008a610115565b005b6100a660048036038101906100a191906102d0565b610189565b005b6100b06101ec565b6040516100bd9190610316565b60405180910390f35b60008173ffffffffffffffffffffffffffffffffffffffff1631905060008273ffffffffffffffffffffffffffffffffffffffff16319050505050565b60046000819055506005600081905550565b6000600190505b6001156101865760008160001b604051602001610139919061035c565b6040516020818303038152906040528051906020012060001c905060008173ffffffffffffffffffffffffffffffffffffffff1631905060018361017d91906103a6565b9250505061011c565b50565b8073ffffffffffffffffffffffffffffffffffffffff1663ac4c25b26040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156101d157600080fd5b505af11580156101e5573d6000803e3d6000fd5b5050505050565b6000806000549050600080549050809250505090565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061023282610207565b9050919050565b61024281610227565b811461024d57600080fd5b50565b60008135905061025f81610239565b92915050565b60006020828403121561027b5761027a610202565b5b600061028984828501610250565b91505092915050565b600061029d82610227565b9050919050565b6102ad81610292565b81146102b857600080fd5b50565b6000813590506102ca816102a4565b92915050565b6000602082840312156102e6576102e5610202565b5b60006102f4848285016102bb565b91505092915050565b6000819050919050565b610310816102fd565b82525050565b600060208201905061032b6000830184610307565b92915050565b6000819050919050565b6000819050919050565b61035661035182610331565b61033b565b82525050565b60006103688284610345565b60208201915081905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006103b1826102fd565b91506103bc836102fd565b92508282019050808211156103d4576103d3610377565b5b9291505056fea26469706673582212202182fa69ea8e39cf4dcbadb7f3ccba2544ba38c9be05a5087cd41f59a174e1d164736f6c63430008120033 \ No newline at end of file diff --git a/frame/evm/src/runner/mod.rs b/frame/evm/src/runner/mod.rs index 179f1eae69..b23379690b 100644 --- a/frame/evm/src/runner/mod.rs +++ b/frame/evm/src/runner/mod.rs @@ -17,7 +17,7 @@ pub mod stack; -use crate::Config; +use crate::{Config, Weight}; use fp_evm::{CallInfo, CreateInfo}; use sp_core::{H160, H256, U256}; use sp_std::vec::Vec; @@ -25,7 +25,7 @@ use sp_std::vec::Vec; #[derive(Debug)] pub struct RunnerError> { pub error: E, - pub weight: frame_support::weights::Weight, + pub weight: Weight, } pub trait Runner { @@ -42,6 +42,8 @@ pub trait Runner { nonce: Option, access_list: Vec<(H160, Vec)>, is_transactional: bool, + weight_limit: Option, + transaction_len: Option, evm_config: &evm::Config, ) -> Result<(), RunnerError>; @@ -57,6 +59,8 @@ pub trait Runner { access_list: Vec<(H160, Vec)>, is_transactional: bool, validate: bool, + weight_limit: Option, + transaction_len: Option, config: &evm::Config, ) -> Result>; @@ -71,6 +75,8 @@ pub trait Runner { access_list: Vec<(H160, Vec)>, is_transactional: bool, validate: bool, + weight_limit: Option, + transaction_len: Option, config: &evm::Config, ) -> Result>; @@ -86,6 +92,8 @@ pub trait Runner { access_list: Vec<(H160, Vec)>, is_transactional: bool, validate: bool, + weight_limit: Option, + transaction_len: Option, config: &evm::Config, ) -> Result>; } diff --git a/frame/evm/src/runner/stack.rs b/frame/evm/src/runner/stack.rs index f21a8b1fbd..cdff3369ce 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -18,18 +18,22 @@ //! EVM stack-based runner. use crate::{ - runner::Runner as RunnerT, AccountCodes, AccountStorages, AddressMapping, BalanceOf, - BlockHashMapping, Config, Error, Event, FeeCalculator, OnChargeEVMTransaction, OnCreate, - Pallet, RunnerError, + runner::Runner as RunnerT, AccountCodes, AccountCodesMetadata, AccountStorages, AddressMapping, + BalanceOf, BlockHashMapping, Config, Error, Event, FeeCalculator, OnChargeEVMTransaction, + OnCreate, Pallet, RunnerError, Weight, }; use evm::{ backend::Backend as BackendT, executor::stack::{Accessed, StackExecutor, StackState as StackStateT, StackSubstateMetadata}, - ExitError, ExitReason, Transfer, + gasometer::{GasCost, StorageTarget}, + ExitError, ExitReason, Opcode, Transfer, }; use fp_evm::{ - CallInfo, CreateInfo, ExecutionInfo, IsPrecompileResult, Log, PrecompileSet, Vicinity, + AccessedStorage, CallInfo, CreateInfo, ExecutionInfoV2, IsPrecompileResult, Log, PrecompileSet, + Vicinity, WeightInfo, ACCOUNT_BASIC_PROOF_SIZE, ACCOUNT_CODES_METADATA_PROOF_SIZE, + ACCOUNT_STORAGE_PROOF_SIZE, IS_EMPTY_CHECK_PROOF_SIZE, WRITE_PROOF_SIZE, }; + use frame_support::traits::{Currency, ExistenceRequirement, Get, Time}; use sp_core::{H160, H256, U256}; use sp_runtime::traits::UniqueSaturatedInto; @@ -64,8 +68,10 @@ where config: &'config evm::Config, precompiles: &'precompiles T::PrecompilesType, is_transactional: bool, + weight_limit: Option, + proof_size_base_cost: Option, f: F, - ) -> Result, RunnerError>> + ) -> Result, RunnerError>> where F: FnOnce( &mut StackExecutor< @@ -99,6 +105,8 @@ where f, base_fee, weight, + weight_limit, + proof_size_base_cost, ); // Set IN_EVM to false @@ -121,8 +129,10 @@ where is_transactional: bool, f: F, base_fee: U256, - weight: crate::Weight, - ) -> Result, RunnerError>> + weight: Weight, + weight_limit: Option, + proof_size_base_cost: Option, + ) -> Result, RunnerError>> where F: FnOnce( &mut StackExecutor< @@ -134,21 +144,29 @@ where ) -> (ExitReason, R), R: Default, { + // Used to record the external costs in the evm through the StackState implementation + let maybe_weight_info = + WeightInfo::new_from_weight_limit(weight_limit, proof_size_base_cost).map_err( + |_| RunnerError { + error: Error::::Undefined, + weight, + }, + )?; // The precompile check is only used for transactional invocations. However, here we always // execute the check, because the check has side effects. - let is_precompile = match precompiles.is_precompile(source, gas_limit) { - IsPrecompileResult::Answer { - is_precompile, - extra_cost, - } => { + match precompiles.is_precompile(source, gas_limit) { + IsPrecompileResult::Answer { extra_cost, .. } => { gas_limit = gas_limit.saturating_sub(extra_cost); - is_precompile } IsPrecompileResult::OutOfGas => { - return Ok(ExecutionInfo { + return Ok(ExecutionInfoV2 { exit_reason: ExitError::OutOfGas.into(), value: Default::default(), - used_gas: gas_limit.into(), + used_gas: fp_evm::UsedGas { + standard: gas_limit.into(), + effective: gas_limit.into(), + }, + weight_info: maybe_weight_info, logs: Default::default(), }) } @@ -160,12 +178,7 @@ where // // EIP-3607: https://eips.ethereum.org/EIPS/eip-3607 // Do not allow transactions for which `tx.sender` has any code deployed. - // - // We extend the principle of this EIP to also prevent `tx.sender` to be the address - // of a precompile. While mainnet Ethereum currently only has stateless precompiles, - // projects using Frontier can have stateful precompiles that can manage funds or - // which calls other contracts that expects this precompile address to be trustworthy. - if is_transactional && (!>::get(source).is_empty() || is_precompile) { + if is_transactional && !>::get(source).is_empty() { return Err(RunnerError { error: Error::::TransactionMustComeFromEOA, weight, @@ -222,14 +235,25 @@ where }; let metadata = StackSubstateMetadata::new(gas_limit, config); - let state = SubstrateStackState::new(&vicinity, metadata); + let state = SubstrateStackState::new(&vicinity, metadata, maybe_weight_info); let mut executor = StackExecutor::new_with_precompiles(state, config, precompiles); let (reason, retv) = f(&mut executor); // Post execution. - let used_gas = U256::from(executor.used_gas()); - let actual_fee = executor.fee(total_fee_per_gas); + let used_gas = executor.used_gas(); + let effective_gas = match executor.state().weight_info() { + Some(weight_info) => U256::from(sp_std::cmp::max( + used_gas, + weight_info + .proof_size_usage + .unwrap_or_default() + .saturating_mul(T::GasLimitPovSizeRatio::get()), + )), + _ => used_gas.into(), + }; + let actual_fee = effective_gas.saturating_mul(total_fee_per_gas); + log::debug!( target: "evm", "Execution {:?} [source: {:?}, value: {}, gas_limit: {}, actual_fee: {}, is_transactional: {}]", @@ -274,13 +298,13 @@ where let state = executor.into_state(); - for address in state.substate.deletes { + for address in &state.substate.deletes { log::debug!( target: "evm", "Deleting account at {:?}", address ); - Pallet::::remove_account(&address) + Pallet::::remove_account(address) } for log in &state.substate.logs { @@ -302,10 +326,14 @@ where }); } - Ok(ExecutionInfo { + Ok(ExecutionInfoV2 { value: retv, exit_reason: reason, - used_gas, + used_gas: fp_evm::UsedGas { + standard: used_gas.into(), + effective: effective_gas, + }, + weight_info: state.weight_info(), logs: state.substate.logs, }) } @@ -328,6 +356,8 @@ where nonce: Option, access_list: Vec<(H160, Vec)>, is_transactional: bool, + weight_limit: Option, + proof_size_base_cost: Option, evm_config: &evm::Config, ) -> Result<(), RunnerError> { let (base_fee, mut weight) = T::FeeCalculator::min_gas_price(); @@ -354,6 +384,8 @@ where value, access_list, }, + weight_limit, + proof_size_base_cost, ) .validate_in_block_for(&source_account) .and_then(|v| v.with_base_fee()) @@ -374,6 +406,8 @@ where access_list: Vec<(H160, Vec)>, is_transactional: bool, validate: bool, + weight_limit: Option, + proof_size_base_cost: Option, config: &evm::Config, ) -> Result> { if validate { @@ -388,6 +422,8 @@ where nonce, access_list.clone(), is_transactional, + weight_limit, + proof_size_base_cost, config, )?; } @@ -401,6 +437,8 @@ where config, &precompiles, is_transactional, + weight_limit, + proof_size_base_cost, |executor| executor.transact_call(source, target, value, input, gas_limit, access_list), ) } @@ -416,6 +454,8 @@ where access_list: Vec<(H160, Vec)>, is_transactional: bool, validate: bool, + weight_limit: Option, + proof_size_base_cost: Option, config: &evm::Config, ) -> Result> { if validate { @@ -430,6 +470,8 @@ where nonce, access_list.clone(), is_transactional, + weight_limit, + proof_size_base_cost, config, )?; } @@ -443,6 +485,8 @@ where config, &precompiles, is_transactional, + weight_limit, + proof_size_base_cost, |executor| { let address = executor.create_address(evm::CreateScheme::Legacy { caller: source }); T::OnCreate::on_create(source, address); @@ -465,6 +509,8 @@ where access_list: Vec<(H160, Vec)>, is_transactional: bool, validate: bool, + weight_limit: Option, + proof_size_base_cost: Option, config: &evm::Config, ) -> Result> { if validate { @@ -479,6 +525,8 @@ where nonce, access_list.clone(), is_transactional, + weight_limit, + proof_size_base_cost, config, )?; } @@ -493,6 +541,8 @@ where config, &precompiles, is_transactional, + weight_limit, + proof_size_base_cost, |executor| { let address = executor.create_address(evm::CreateScheme::Create2 { caller: source, @@ -605,17 +655,29 @@ impl<'config> SubstrateStackSubstate<'config> { } } +#[derive(Default, Clone, Eq, PartialEq)] +pub struct Recorded { + account_codes: sp_std::vec::Vec, + account_storages: BTreeMap<(H160, H256), bool>, +} + /// Substrate backend for EVM. pub struct SubstrateStackState<'vicinity, 'config, T> { vicinity: &'vicinity Vicinity, substate: SubstrateStackSubstate<'config>, original_storage: BTreeMap<(H160, H256), H256>, + recorded: Recorded, + weight_info: Option, _marker: PhantomData, } impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> { /// Create a new backend with given vicinity. - pub fn new(vicinity: &'vicinity Vicinity, metadata: StackSubstateMetadata<'config>) -> Self { + pub fn new( + vicinity: &'vicinity Vicinity, + metadata: StackSubstateMetadata<'config>, + weight_info: Option, + ) -> Self { Self { vicinity, substate: SubstrateStackSubstate { @@ -626,11 +688,28 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> { }, _marker: PhantomData, original_storage: BTreeMap::new(), + recorded: Default::default(), + weight_info, } } + + pub fn weight_info(&self) -> Option { + self.weight_info + } + + pub fn recorded(&self) -> &Recorded { + &self.recorded + } + + pub fn info_mut(&mut self) -> (&mut Option, &mut Recorded) { + (&mut self.weight_info, &mut self.recorded) + } } -impl<'vicinity, 'config, T: Config> BackendT for SubstrateStackState<'vicinity, 'config, T> { +impl<'vicinity, 'config, T: Config> BackendT for SubstrateStackState<'vicinity, 'config, T> +where + BalanceOf: TryFrom + Into, +{ fn gas_price(&self) -> U256 { self.vicinity.gas_price } @@ -689,6 +768,61 @@ impl<'vicinity, 'config, T: Config> BackendT for SubstrateStackState<'vicinity, } } + fn record_external_operation(&mut self, op: evm::ExternalOperation) -> Result<(), ExitError> { + let size_limit: u64 = self + .metadata() + .gasometer() + .config() + .create_contract_limit + .unwrap_or_default() as u64; + + let (weight_info, recorded) = self.info_mut(); + + if let Some(weight_info) = weight_info { + match op { + evm::ExternalOperation::AccountBasicRead => { + weight_info.try_record_proof_size_or_fail(ACCOUNT_BASIC_PROOF_SIZE)? + } + evm::ExternalOperation::AddressCodeRead(address) => { + let maybe_record = !recorded.account_codes.contains(&address); + // Skip if the address has been already recorded this block + if maybe_record { + // First we record account emptiness check. + // Transfers to EOAs with standard 21_000 gas limit are able to + // pay for this pov size. + weight_info.try_record_proof_size_or_fail(IS_EMPTY_CHECK_PROOF_SIZE)?; + + if >::decode_len(address).unwrap_or(0) == 0 { + return Ok(()); + } + // Try to record fixed sized `AccountCodesMetadata` read + // Tentatively 16 + 20 + 40 + weight_info + .try_record_proof_size_or_fail(ACCOUNT_CODES_METADATA_PROOF_SIZE)?; + if let Some(meta) = >::get(address) { + weight_info.try_record_proof_size_or_fail(meta.size)?; + } else { + // If it does not exist, try to record `create_contract_limit` first. + weight_info.try_record_proof_size_or_fail(size_limit)?; + let meta = Pallet::::account_code_metadata(address); + let actual_size = meta.size; + // Refund if applies + weight_info.refund_proof_size(size_limit.saturating_sub(actual_size)); + } + recorded.account_codes.push(address); + } + } + evm::ExternalOperation::IsEmpty => { + weight_info.try_record_proof_size_or_fail(IS_EMPTY_CHECK_PROOF_SIZE)? + } + evm::ExternalOperation::Write => { + weight_info.try_record_proof_size_or_fail(WRITE_PROOF_SIZE)? + } + }; + } + Ok(()) + } + fn code(&self, address: H160) -> Vec { >::get(address) } @@ -698,8 +832,6 @@ impl<'vicinity, 'config, T: Config> BackendT for SubstrateStackState<'vicinity, } fn original_storage(&self, address: H160, index: H256) -> Option { - // Not being cached means that it was never changed, which means we - // can fetch it from storage. Some( self.original_storage .get(&(address, index)) @@ -816,7 +948,6 @@ where fn transfer(&mut self, transfer: Transfer) -> Result<(), ExitError> { let source = T::AddressMapping::into_account_id(transfer.source); let target = T::AddressMapping::into_account_id(transfer.target); - T::Currency::transfer( &source, &target, @@ -862,6 +993,202 @@ where fn code_hash(&self, address: H160) -> H256 { >::account_code_metadata(address).hash } + + fn record_external_dynamic_opcode_cost( + &mut self, + opcode: Opcode, + _gas_cost: GasCost, + target: evm::gasometer::StorageTarget, + ) -> Result<(), ExitError> { + // If account code or storage slot is in the overlay it is already accounted for and early exit + let mut accessed_storage: Option = match target { + StorageTarget::Address(address) => { + if self.recorded().account_codes.contains(&address) { + return Ok(()); + } else { + Some(AccessedStorage::AccountCodes(address)) + } + } + StorageTarget::Slot(address, index) => { + if self + .recorded() + .account_storages + .contains_key(&(address, index)) + { + return Ok(()); + } else { + Some(AccessedStorage::AccountStorages((address, index))) + } + } + _ => None, + }; + + let size_limit: u64 = self + .metadata() + .gasometer() + .config() + .create_contract_limit + .unwrap_or_default() as u64; + + let (weight_info, recorded) = { + let (weight_info, recorded) = self.info_mut(); + if let Some(weight_info) = weight_info { + (weight_info, recorded) + } else { + return Ok(()); + } + }; + + // Record ref_time first + // TODO benchmark opcodes, until this is done we do used_gas to weight conversion for ref_time + + // Record proof_size + // Return if proof size recording is disabled + let proof_size_limit = if let Some(proof_size_limit) = weight_info.proof_size_limit { + proof_size_limit + } else { + return Ok(()); + }; + + let mut maybe_record_and_refund = |with_empty_check: bool| -> Result<(), ExitError> { + let address = if let Some(AccessedStorage::AccountCodes(address)) = accessed_storage { + address + } else { + // This must be unreachable, a valid target must be set. + // TODO decide how do we want to gracefully handle. + return Err(ExitError::OutOfGas); + }; + // First try to record fixed sized `AccountCodesMetadata` read + // Tentatively 20 + 8 + 32 + let mut base_cost = ACCOUNT_CODES_METADATA_PROOF_SIZE; + if with_empty_check { + base_cost = base_cost.saturating_add(IS_EMPTY_CHECK_PROOF_SIZE); + } + weight_info.try_record_proof_size_or_fail(base_cost)?; + if let Some(meta) = >::get(address) { + weight_info.try_record_proof_size_or_fail(meta.size)?; + } else { + // If it does not exist, try to record `create_contract_limit` first. + weight_info.try_record_proof_size_or_fail(size_limit)?; + let meta = Pallet::::account_code_metadata(address); + let actual_size = meta.size; + // Refund if applies + weight_info.refund_proof_size(size_limit.saturating_sub(actual_size)); + } + recorded.account_codes.push(address); + // Already recorded, return + Ok(()) + }; + + // Proof size is fixed length for writes (a 32-byte hash in a merkle trie), and + // the full key/value for reads. For read and writes over the same storage, the full value + // is included. + // For cold reads involving code (call, callcode, staticcall and delegatecall): + // - We depend on https://github.com/paritytech/frontier/pull/893 + // - Try to get the cached size or compute it on the fly + // - We record the actual size after caching, refunding the difference between it and the initially deducted + // contract size limit. + let opcode_proof_size = match opcode { + // Basic account fixed length + Opcode::BALANCE => { + accessed_storage = None; + U256::from(ACCOUNT_BASIC_PROOF_SIZE) + } + Opcode::EXTCODESIZE | Opcode::EXTCODECOPY | Opcode::EXTCODEHASH => { + return maybe_record_and_refund(false) + } + Opcode::CALLCODE | Opcode::CALL | Opcode::DELEGATECALL | Opcode::STATICCALL => { + return maybe_record_and_refund(true) + } + // (H160, H256) double map blake2 128 concat key size (68) + value 32 + Opcode::SLOAD => U256::from(ACCOUNT_STORAGE_PROOF_SIZE), + Opcode::SSTORE => { + let (address, index) = + if let Some(AccessedStorage::AccountStorages((address, index))) = + accessed_storage + { + (address, index) + } else { + // This must be unreachable, a valid target must be set. + // TODO decide how do we want to gracefully handle. + return Err(ExitError::OutOfGas); + }; + let mut cost = WRITE_PROOF_SIZE; + let maybe_record = !recorded.account_storages.contains_key(&(address, index)); + // If the slot is yet to be accessed we charge for it, as the evm reads + // it prior to the opcode execution. + // Skip if the address and index has been already recorded this block. + if maybe_record { + cost = cost.saturating_add(ACCOUNT_STORAGE_PROOF_SIZE); + } + U256::from(cost) + } + // Fixed trie 32 byte hash + Opcode::CREATE | Opcode::CREATE2 => U256::from(WRITE_PROOF_SIZE), + // When calling SUICIDE a target account will receive the self destructing + // address's balance. We need to account for both: + // - Target basic account read + // - 5 bytes of `decode_len` + Opcode::SUICIDE => { + accessed_storage = None; + U256::from(IS_EMPTY_CHECK_PROOF_SIZE) + } + // Rest of dynamic opcodes that do not involve proof size recording, do nothing + _ => return Ok(()), + }; + + if opcode_proof_size > U256::from(u64::MAX) { + weight_info.try_record_proof_size_or_fail(proof_size_limit)?; + return Err(ExitError::OutOfGas); + } + + // Cache the storage access + match accessed_storage { + Some(AccessedStorage::AccountStorages((address, index))) => { + recorded.account_storages.insert((address, index), true); + } + Some(AccessedStorage::AccountCodes(address)) => { + recorded.account_codes.push(address); + } + _ => {} + } + + // Record cost + self.record_external_cost(None, Some(opcode_proof_size.low_u64()))?; + Ok(()) + } + + fn record_external_cost( + &mut self, + ref_time: Option, + proof_size: Option, + ) -> Result<(), ExitError> { + let weight_info = if let (Some(weight_info), _) = self.info_mut() { + weight_info + } else { + return Ok(()); + }; + // Record ref_time first + // TODO benchmark opcodes, until this is done we do used_gas to weight conversion for ref_time + if let Some(amount) = ref_time { + weight_info.try_record_ref_time_or_fail(amount)?; + } + if let Some(amount) = proof_size { + weight_info.try_record_proof_size_or_fail(amount)?; + } + Ok(()) + } + + fn refund_external_cost(&mut self, ref_time: Option, proof_size: Option) { + if let Some(mut weight_info) = self.weight_info { + if let Some(amount) = ref_time { + weight_info.refund_ref_time(amount); + } + if let Some(amount) = proof_size { + weight_info.refund_proof_size(amount); + } + } + } } #[cfg(feature = "forbid-evm-reentrancy")] @@ -910,7 +1237,7 @@ mod tests { ); assert_matches!( res, - Ok(ExecutionInfo { + Ok(ExecutionInfoV2 { exit_reason: ExitReason::Error(ExitError::CallTooDeep), .. }) diff --git a/frame/evm/src/tests.rs b/frame/evm/src/tests.rs index bb09d665db..bb1850708c 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -26,6 +26,604 @@ use frame_support::{ }; use std::{collections::BTreeMap, str::FromStr}; +mod proof_size_test { + use super::*; + use fp_evm::{ + CreateInfo, ACCOUNT_BASIC_PROOF_SIZE, ACCOUNT_CODES_METADATA_PROOF_SIZE, + ACCOUNT_STORAGE_PROOF_SIZE, IS_EMPTY_CHECK_PROOF_SIZE, WRITE_PROOF_SIZE, + }; + use frame_support::traits::StorageInfoTrait; + // pragma solidity ^0.8.2; + // contract Callee { + // // ac4c25b2 + // function void() public { + // uint256 foo = 1; + // } + // } + pub const PROOF_SIZE_TEST_CALLEE_CONTRACT_BYTECODE: &str = + include_str!("./res/proof_size_test_callee_contract_bytecode.txt"); + // pragma solidity ^0.8.2; + // contract ProofSizeTest { + // uint256 foo; + // constructor() { + // foo = 6; + // } + // // 35f56c3b + // function test_balance(address who) public { + // // cold + // uint256 a = address(who).balance; + // // warm + // uint256 b = address(who).balance; + // } + // // e27a0ecd + // function test_sload() public returns (uint256) { + // // cold + // uint256 a = foo; + // // warm + // uint256 b = foo; + // return b; + // } + // // 4f3080a9 + // function test_sstore() public { + // // cold + // foo = 4; + // // warm + // foo = 5; + // } + // // c6d6f606 + // function test_call(Callee _callee) public { + // _callee.void(); + // } + // // 944ddc62 + // function test_oog() public { + // uint256 i = 1; + // while(true) { + // address who = address(uint160(uint256(keccak256(abi.encodePacked(bytes32(i)))))); + // uint256 a = address(who).balance; + // i = i + 1; + // } + // } + // } + pub const PROOF_SIZE_TEST_CONTRACT_BYTECODE: &str = + include_str!("./res/proof_size_test_contract_bytecode.txt"); + + fn create_proof_size_test_callee_contract( + gas_limit: u64, + weight_limit: Option, + ) -> Result>> { + ::Runner::create( + H160::default(), + hex::decode(PROOF_SIZE_TEST_CALLEE_CONTRACT_BYTECODE.trim_end()).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + weight_limit, + Some(0), + &::config().clone(), + ) + } + + fn create_proof_size_test_contract( + gas_limit: u64, + weight_limit: Option, + ) -> Result>> { + ::Runner::create( + H160::default(), + hex::decode(PROOF_SIZE_TEST_CONTRACT_BYTECODE.trim_end()).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // non-transactional + true, // must be validated + weight_limit, + Some(0), + &::config().clone(), + ) + } + + #[test] + fn account_basic_proof_size_constant_matches() { + assert_eq!( + ACCOUNT_BASIC_PROOF_SIZE, + frame_system::Account::::storage_info() + .get(0) + .expect("item") + .max_size + .expect("size") as u64 + ); + } + + #[test] + fn account_storage_proof_size_constant_matches() { + assert_eq!( + ACCOUNT_STORAGE_PROOF_SIZE, + AccountStorages::::storage_info() + .get(0) + .expect("item") + .max_size + .expect("size") as u64 + ); + } + + #[test] + fn account_codes_metadata_proof_size_constant_matches() { + assert_eq!( + ACCOUNT_CODES_METADATA_PROOF_SIZE, + AccountCodesMetadata::::storage_info() + .get(0) + .expect("item") + .max_size + .expect("size") as u64 + ); + } + + #[test] + fn proof_size_create_accounting_works() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 1_000_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + let result = create_proof_size_test_callee_contract(gas_limit, Some(weight_limit)) + .expect("create succeeds"); + + // Creating a new contract does not involve reading the code from storage. + // We account for a fixed hash proof size write, an empty check and . + let write_cost = WRITE_PROOF_SIZE; + let is_empty_check = IS_EMPTY_CHECK_PROOF_SIZE; + let nonce_increases = ACCOUNT_BASIC_PROOF_SIZE * 2; + let expected_proof_size = write_cost + is_empty_check + nonce_increases; + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn proof_size_subcall_accounting_works() { + new_test_ext().execute_with(|| { + // Create callee contract A + let gas_limit: u64 = 1_000_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + let result = + create_proof_size_test_callee_contract(gas_limit, None).expect("create succeeds"); + + let subcall_contract_address = result.value; + + // Create proof size test contract B + let result = create_proof_size_test_contract(gas_limit, None).expect("create succeeds"); + + let call_contract_address = result.value; + + // Call B, that calls A, with weight limit + // selector for ProofSizeTest::test_call function.. + let mut call_data: String = "c6d6f606000000000000000000000000".to_owned(); + // ..encode the callee address argument + call_data.push_str(&format!("{:x}", subcall_contract_address)); + + let result = ::Runner::call( + H160::default(), + call_contract_address, + hex::decode(&call_data).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &::config().clone(), + ) + .expect("call succeeds"); + + // Expected proof size + let reading_main_contract_len = AccountCodes::::get(call_contract_address).len(); + let reading_contract_len = AccountCodes::::get(subcall_contract_address).len(); + let read_account_metadata = ACCOUNT_CODES_METADATA_PROOF_SIZE as usize; + let is_empty_check = (IS_EMPTY_CHECK_PROOF_SIZE * 2) as usize; + let increase_nonce = (ACCOUNT_BASIC_PROOF_SIZE * 3) as usize; + let expected_proof_size = ((read_account_metadata * 2) + + reading_contract_len + + reading_main_contract_len + + is_empty_check + increase_nonce) as u64; + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn proof_size_balance_accounting_works() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 1_000_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + // Create proof size test contract + let result = create_proof_size_test_contract(gas_limit, None).expect("create succeeds"); + + let call_contract_address = result.value; + + // selector for ProofSizeTest::balance function.. + let mut call_data: String = "35f56c3b000000000000000000000000".to_owned(); + // ..encode bobs address + call_data.push_str(&format!("{:x}", H160::random())); + + let result = ::Runner::call( + H160::default(), + call_contract_address, + hex::decode(&call_data).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &::config().clone(), + ) + .expect("call succeeds"); + + // - Three account reads. + // - Main contract code read. + // - One metadata read. + let basic_account_size = (ACCOUNT_BASIC_PROOF_SIZE * 3) as usize; + let read_account_metadata = ACCOUNT_CODES_METADATA_PROOF_SIZE as usize; + let is_empty_check = IS_EMPTY_CHECK_PROOF_SIZE as usize; + let increase_nonce = ACCOUNT_BASIC_PROOF_SIZE as usize; + let reading_main_contract_len = AccountCodes::::get(call_contract_address).len(); + let expected_proof_size = (basic_account_size + + read_account_metadata + + reading_main_contract_len + + is_empty_check + increase_nonce) as u64; + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn proof_size_sload_accounting_works() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 1_000_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + // Create proof size test contract + let result = create_proof_size_test_contract(gas_limit, None).expect("create succeeds"); + + let call_contract_address = result.value; + + // selector for ProofSizeTest::test_sload function.. + let call_data: String = "e27a0ecd".to_owned(); + let result = ::Runner::call( + H160::default(), + call_contract_address, + hex::decode(&call_data).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &::config().clone(), + ) + .expect("call succeeds"); + + let reading_main_contract_len = + AccountCodes::::get(call_contract_address).len() as u64; + let expected_proof_size = reading_main_contract_len + + ACCOUNT_STORAGE_PROOF_SIZE + + ACCOUNT_CODES_METADATA_PROOF_SIZE + + IS_EMPTY_CHECK_PROOF_SIZE + + (ACCOUNT_BASIC_PROOF_SIZE * 2); + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn proof_size_sstore_accounting_works() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 1_000_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + // Create proof size test contract + let result = create_proof_size_test_contract(gas_limit, None).expect("create succeeds"); + + let call_contract_address = result.value; + + // selector for ProofSizeTest::test_sstore function.. + let call_data: String = "4f3080a9".to_owned(); + let result = ::Runner::call( + H160::default(), + call_contract_address, + hex::decode(&call_data).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &::config().clone(), + ) + .expect("call succeeds"); + + let reading_main_contract_len = + AccountCodes::::get(call_contract_address).len() as u64; + let expected_proof_size = reading_main_contract_len + + WRITE_PROOF_SIZE + + ACCOUNT_CODES_METADATA_PROOF_SIZE + + ACCOUNT_STORAGE_PROOF_SIZE + + IS_EMPTY_CHECK_PROOF_SIZE + + (ACCOUNT_BASIC_PROOF_SIZE * 2); + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn proof_size_oog_works() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 1_000_000; + let mut weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + // Artifically set a lower proof size limit so we OOG this instead gas. + *weight_limit.proof_size_mut() = weight_limit.proof_size() / 2; + + // Create proof size test contract + let result = create_proof_size_test_contract(gas_limit, None).expect("create succeeds"); + + let call_contract_address = result.value; + + // selector for ProofSizeTest::test_oog function.. + let call_data: String = "944ddc62".to_owned(); + let result = ::Runner::call( + H160::default(), + call_contract_address, + hex::decode(&call_data).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &::config().clone(), + ) + .expect("call succeeds"); + + // Find how many random balance reads can we do with the available proof size. + let reading_main_contract_len = + AccountCodes::::get(call_contract_address).len() as u64; + let overhead = reading_main_contract_len + + ACCOUNT_CODES_METADATA_PROOF_SIZE + + IS_EMPTY_CHECK_PROOF_SIZE; + let available_proof_size = weight_limit.proof_size() - overhead; + let number_balance_reads = + available_proof_size.saturating_div(ACCOUNT_BASIC_PROOF_SIZE); + // The actual proof size consumed by those balance reads. + let expected_proof_size = + overhead + (number_balance_reads * ACCOUNT_BASIC_PROOF_SIZE) as u64; + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn uncached_account_code_proof_size_accounting_works() { + new_test_ext().execute_with(|| { + // Create callee contract A + let gas_limit: u64 = 1_000_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + let result = + create_proof_size_test_callee_contract(gas_limit, None).expect("create succeeds"); + + let subcall_contract_address = result.value; + + // Expect callee contract code hash and size to be cached + let _ = >::get(subcall_contract_address) + .expect("contract code hash and size are cached"); + + // Remove callee cache + >::remove(subcall_contract_address); + + // Create proof size test contract B + let result = create_proof_size_test_contract(gas_limit, None).expect("create succeeds"); + + let call_contract_address = result.value; + + // Call B, that calls A, with weight limit + // selector for ProofSizeTest::test_call function.. + let mut call_data: String = "c6d6f606000000000000000000000000".to_owned(); + // ..encode the callee address argument + call_data.push_str(&format!("{:x}", subcall_contract_address)); + let result = ::Runner::call( + H160::default(), + call_contract_address, + hex::decode(&call_data).unwrap(), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &::config().clone(), + ) + .expect("call succeeds"); + + // Expected proof size + let read_account_metadata = ACCOUNT_CODES_METADATA_PROOF_SIZE as usize; + let is_empty_check = (IS_EMPTY_CHECK_PROOF_SIZE * 2) as usize; + let increase_nonce = (ACCOUNT_BASIC_PROOF_SIZE * 3) as usize; + let reading_main_contract_len = AccountCodes::::get(call_contract_address).len(); + let reading_callee_contract_len = + AccountCodes::::get(subcall_contract_address).len(); + // In order to do the subcall, we need to check metadata 3 times - + // one for each contract + one for the call opcode -, load two bytecodes - caller and callee. + let expected_proof_size = ((read_account_metadata * 2) + + reading_callee_contract_len + + reading_main_contract_len + + is_empty_check + increase_nonce) as u64; + + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(expected_proof_size, actual_proof_size); + }); + } + + #[test] + fn proof_size_breaks_standard_transfer() { + new_test_ext().execute_with(|| { + // In this test we do a simple transfer to an address with an stored code which is + // greater in size (and thus load cost) than the transfer flat fee of 21_000. + + // We assert that providing 21_000 gas limit will not work, because the pov size limit + // will OOG. + let fake_contract_address = H160::random(); + let config = ::config().clone(); + let fake_contract_code = vec![0; config.create_contract_limit.expect("a value")]; + AccountCodes::::insert(fake_contract_address, fake_contract_code); + + let gas_limit: u64 = 21_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + let result = ::Runner::call( + H160::default(), + fake_contract_address, + Vec::new(), + U256::from(777), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &config, + ) + .expect("call succeeds"); + + assert_eq!( + result.exit_reason, + crate::ExitReason::Error(crate::ExitError::OutOfGas) + ); + }); + } + + #[test] + fn proof_size_based_refunding_works() { + new_test_ext().execute_with(|| { + // In this test we do a simple transfer to an address with an stored code which is + // greater in size (and thus load cost) than the transfer flat fee of 21_000. + + // Assert that if we provide enough gas limit, the refund will be based on the pov + // size consumption, not the 21_000 gas. + let fake_contract_address = H160::random(); + let config = ::config().clone(); + let fake_contract_code = vec![0; config.create_contract_limit.expect("a value")]; + AccountCodes::::insert(fake_contract_address, fake_contract_code); + + let gas_limit: u64 = 700_000; + let weight_limit = FixedGasWeightMapping::::gas_to_weight(gas_limit, true); + + let result = ::Runner::call( + H160::default(), + fake_contract_address, + Vec::new(), + U256::from(777), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(weight_limit), + Some(0), + &config, + ) + .expect("call succeeds"); + + let ratio = <::GasLimitPovSizeRatio as Get>::get(); + let used_gas = result.used_gas; + let actual_proof_size = result + .weight_info + .expect("weight info") + .proof_size_usage + .expect("proof size usage"); + + assert_eq!(used_gas.standard, U256::from(21_000)); + assert_eq!(used_gas.effective, U256::from(actual_proof_size * ratio)); + }); + } +} + type Balances = pallet_balances::Pallet; type EVM = Pallet; @@ -461,6 +1059,8 @@ fn runner_non_transactional_calls_with_non_balance_accounts_is_ok_without_gas_pr Vec::new(), false, // non-transactional true, // must be validated + None, + None, &::config().clone(), ) .expect("Non transactional call succeeds"); @@ -495,6 +1095,8 @@ fn runner_non_transactional_calls_with_non_balance_accounts_is_err_with_gas_pric Vec::new(), false, // non-transactional true, // must be validated + None, + None, &::config().clone(), ); assert!(res.is_err()); @@ -517,6 +1119,8 @@ fn runner_transactional_call_with_zero_gas_price_fails() { Vec::new(), true, // transactional true, // must be validated + None, + None, &::config().clone(), ); assert!(res.is_err()); @@ -539,6 +1143,8 @@ fn runner_max_fee_per_gas_gte_max_priority_fee_per_gas() { Vec::new(), true, // transactional true, // must be validated + None, + None, &::config().clone(), ); assert!(res.is_err()); @@ -554,6 +1160,8 @@ fn runner_max_fee_per_gas_gte_max_priority_fee_per_gas() { Vec::new(), false, // non-transactional true, // must be validated + None, + None, &::config().clone(), ); assert!(res.is_err()); @@ -577,6 +1185,8 @@ fn eip3607_transaction_from_contract() { Vec::new(), true, // transactional false, // not sure be validated + None, + None, &::config().clone(), ) { Err(RunnerError { @@ -600,52 +1210,8 @@ fn eip3607_transaction_from_contract() { Vec::new(), false, // non-transactional true, // must be validated - &::config().clone(), - ) - .is_ok()); - }); -} - -#[test] -fn eip3607_transaction_from_precompile() { - new_test_ext().execute_with(|| { - // external transaction - match ::Runner::call( - // Precompile address. - H160::from_str("0000000000000000000000000000000000000001").unwrap(), - H160::from_str("1000000000000000000000000000000000000001").unwrap(), - Vec::new(), - U256::from(1u32), - 1000000, - None, None, None, - Vec::new(), - true, // transactional - false, // not sure be validated - &::config().clone(), - ) { - Err(RunnerError { - error: Error::TransactionMustComeFromEOA, - .. - }) => (), - _ => panic!("Should have failed"), - } - - // internal call - assert!(::Runner::call( - // Contract address. - H160::from_str("0000000000000000000000000000000000000001").unwrap(), - H160::from_str("1000000000000000000000000000000000000001").unwrap(), - Vec::new(), - U256::from(1u32), - 1000000, - None, - None, - None, - Vec::new(), - false, // non-transactional - true, // must be validated &::config().clone(), ) .is_ok()); diff --git a/frame/evm/test-vector-support/src/lib.rs b/frame/evm/test-vector-support/src/lib.rs index bfa41719b4..3566840969 100644 --- a/frame/evm/test-vector-support/src/lib.rs +++ b/frame/evm/test-vector-support/src/lib.rs @@ -80,6 +80,12 @@ impl PrecompileHandle for MockHandle { Ok(()) } + fn record_external_cost(&mut self, _: Option, _: Option) -> Result<(), ExitError> { + Ok(()) + } + + fn refund_external_cost(&mut self, _: Option, _: Option) {} + fn log(&mut self, _: H160, _: Vec, _: Vec) -> Result<(), ExitError> { unimplemented!() } diff --git a/primitives/evm/src/lib.rs b/primitives/evm/src/lib.rs index a0fcce9cd6..8f618c4b20 100644 --- a/primitives/evm/src/lib.rs +++ b/primitives/evm/src/lib.rs @@ -25,21 +25,20 @@ use frame_support::weights::{constants::WEIGHT_REF_TIME_PER_MILLIS, Weight}; use scale_codec::{Decode, Encode}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; -use sp_core::{H160, U256}; +use sp_core::{H160, H256, U256}; use sp_runtime::Perbill; use sp_std::vec::Vec; pub use evm::{ backend::{Basic as Account, Log}, - executor::stack::IsPrecompileResult, - Config, ExitReason, + Config, ExitReason, Opcode, }; pub use self::{ precompile::{ - Context, ExitError, ExitRevert, ExitSucceed, LinearCostPrecompile, Precompile, - PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, PrecompileSet, - Transfer, + Context, ExitError, ExitRevert, ExitSucceed, IsPrecompileResult, LinearCostPrecompile, + Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, + PrecompileSet, Transfer, }, validation::{ CheckEvmTransaction, CheckEvmTransactionConfig, CheckEvmTransactionInput, @@ -57,17 +56,124 @@ pub struct Vicinity { pub origin: H160, } +/// `System::Account` 16(hash) + 20 (key) + 52 (AccountInfo::max_encoded_len) +pub const ACCOUNT_BASIC_PROOF_SIZE: u64 = 88; +/// `AccountCodesMetadata` read, temptatively 16 (hash) + 20 (key) + 40 (CodeMetadata). +pub const ACCOUNT_CODES_METADATA_PROOF_SIZE: u64 = 76; +/// 16 (hash1) + 20 (key1) + 16 (hash2) + 32 (key2) + 32 (value) +pub const ACCOUNT_STORAGE_PROOF_SIZE: u64 = 116; +/// Fixed trie 32 byte hash. +pub const WRITE_PROOF_SIZE: u64 = 32; +/// Account basic proof size + 5 bytes max of `decode_len` call. +pub const IS_EMPTY_CHECK_PROOF_SIZE: u64 = 93; + +pub enum AccessedStorage { + AccountCodes(H160), + AccountStorages((H160, H256)), +} + +#[derive(Clone, Copy, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub struct WeightInfo { + pub ref_time_limit: Option, + pub proof_size_limit: Option, + pub ref_time_usage: Option, + pub proof_size_usage: Option, +} + +impl WeightInfo { + pub fn new_from_weight_limit( + weight_limit: Option, + transaction_len: Option, + ) -> Result, &'static str> { + Ok(match (weight_limit, transaction_len) { + (None, _) => None, + (Some(weight_limit), Some(transaction_len)) + if weight_limit.proof_size() >= transaction_len => + { + Some(WeightInfo { + ref_time_limit: Some(weight_limit.ref_time()), + proof_size_limit: Some(weight_limit.proof_size()), + ref_time_usage: Some(0u64), + proof_size_usage: Some(transaction_len), + }) + } + (Some(weight_limit), None) => Some(WeightInfo { + ref_time_limit: Some(weight_limit.ref_time()), + proof_size_limit: None, + ref_time_usage: Some(0u64), + proof_size_usage: None, + }), + _ => return Err("must provide Some valid weight limit or None"), + }) + } + fn try_consume(&self, cost: u64, limit: u64, usage: u64) -> Result { + let usage = usage.checked_add(cost).ok_or(ExitError::OutOfGas)?; + if usage > limit { + return Err(ExitError::OutOfGas); + } + Ok(usage) + } + pub fn try_record_ref_time_or_fail(&mut self, cost: u64) -> Result<(), ExitError> { + if let (Some(ref_time_usage), Some(ref_time_limit)) = + (self.ref_time_usage, self.ref_time_limit) + { + let ref_time_usage = self.try_consume(cost, ref_time_limit, ref_time_usage)?; + if ref_time_usage > ref_time_limit { + return Err(ExitError::OutOfGas); + } + self.ref_time_usage = Some(ref_time_usage); + } + Ok(()) + } + pub fn try_record_proof_size_or_fail(&mut self, cost: u64) -> Result<(), ExitError> { + if let (Some(proof_size_usage), Some(proof_size_limit)) = + (self.proof_size_usage, self.proof_size_limit) + { + let proof_size_usage = self.try_consume(cost, proof_size_limit, proof_size_usage)?; + if proof_size_usage > proof_size_limit { + return Err(ExitError::OutOfGas); + } + self.proof_size_usage = Some(proof_size_usage); + } + Ok(()) + } + pub fn refund_proof_size(&mut self, amount: u64) { + if let Some(proof_size_usage) = self.proof_size_usage { + let proof_size_usage = proof_size_usage.saturating_sub(amount); + self.proof_size_usage = Some(proof_size_usage); + } + } + pub fn refund_ref_time(&mut self, amount: u64) { + if let Some(ref_time_usage) = self.ref_time_usage { + let ref_time_usage = ref_time_usage.saturating_sub(amount); + self.ref_time_usage = Some(ref_time_usage); + } + } +} + #[derive(Clone, Eq, PartialEq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] -pub struct ExecutionInfo { +pub struct UsedGas { + /// The used_gas as returned by the evm gasometer on exit. + pub standard: U256, + /// The result of applying a gas ratio to the most used + /// external metric during the evm execution. + pub effective: U256, +} + +#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub struct ExecutionInfoV2 { pub exit_reason: ExitReason, pub value: T, - pub used_gas: U256, + pub used_gas: UsedGas, + pub weight_info: Option, pub logs: Vec, } -pub type CallInfo = ExecutionInfo>; -pub type CreateInfo = ExecutionInfo; +pub type CallInfo = ExecutionInfoV2>; +pub type CreateInfo = ExecutionInfoV2; #[derive(Clone, Eq, PartialEq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] @@ -76,6 +182,15 @@ pub enum CallOrCreateInfo { Create(CreateInfo), } +#[derive(Clone, Eq, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] +pub struct ExecutionInfo { + pub exit_reason: ExitReason, + pub value: T, + pub used_gas: U256, + pub logs: Vec, +} + /// Account definition used for genesis block construction. #[cfg(feature = "std")] #[derive(Clone, Eq, PartialEq, Encode, Decode, Debug, Serialize, Deserialize)] diff --git a/primitives/evm/src/precompile.rs b/primitives/evm/src/precompile.rs index 4996445cb6..aab7299d27 100644 --- a/primitives/evm/src/precompile.rs +++ b/primitives/evm/src/precompile.rs @@ -16,7 +16,9 @@ // limitations under the License. pub use evm::{ - executor::stack::{PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileSet}, + executor::stack::{ + IsPrecompileResult, PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileSet, + }, Context, ExitError, ExitRevert, ExitSucceed, Transfer, }; use sp_std::vec::Vec; diff --git a/primitives/evm/src/validation.rs b/primitives/evm/src/validation.rs index 3627f1d64d..a9ea6963d1 100644 --- a/primitives/evm/src/validation.rs +++ b/primitives/evm/src/validation.rs @@ -17,7 +17,7 @@ #![allow(clippy::comparison_chain)] pub use evm::backend::Basic as Account; -use frame_support::sp_runtime::traits::UniqueSaturatedInto; +use frame_support::{sp_runtime::traits::UniqueSaturatedInto, weights::Weight}; use sp_core::{H160, H256, U256}; use sp_std::vec::Vec; @@ -48,6 +48,8 @@ pub struct CheckEvmTransactionConfig<'config> { pub struct CheckEvmTransaction<'config, E: From> { pub config: CheckEvmTransactionConfig<'config>, pub transaction: CheckEvmTransactionInput, + pub weight_limit: Option, + pub proof_size_base_cost: Option, _marker: sp_std::marker::PhantomData, } @@ -68,10 +70,14 @@ impl<'config, E: From> CheckEvmTransaction<'config, pub fn new( config: CheckEvmTransactionConfig<'config>, transaction: CheckEvmTransactionInput, + weight_limit: Option, + proof_size_base_cost: Option, ) -> Self { CheckEvmTransaction { config, transaction, + weight_limit, + proof_size_base_cost, _marker: Default::default(), } } @@ -178,6 +184,17 @@ impl<'config, E: From> CheckEvmTransaction<'config, pub fn validate_common(&self) -> Result<&Self, E> { if self.config.is_transactional { + // Try to subtract the proof_size_base_cost from the Weight proof_size limit or fail. + // Validate the weight limit can afford recording the proof size cost. + if let (Some(weight_limit), Some(proof_size_base_cost)) = + (self.weight_limit, self.proof_size_base_cost) + { + let _ = weight_limit + .proof_size() + .checked_sub(proof_size_base_cost) + .ok_or(InvalidEvmTransactionError::GasLimitTooLow)?; + } + // We must ensure a transaction can pay the cost of its data bytes. // If it can't it should not be included in a block. let mut gasometer = evm::gasometer::Gasometer::new( @@ -257,6 +274,8 @@ mod tests { pub max_fee_per_gas: Option, pub max_priority_fee_per_gas: Option, pub value: U256, + pub weight_limit: Option, + pub proof_size_base_cost: Option, } impl Default for TestCase { @@ -273,6 +292,8 @@ mod tests { max_fee_per_gas: Some(U256::from(1_000_000_000u128)), max_priority_fee_per_gas: Some(U256::from(1_000_000_000u128)), value: U256::from(1u8), + weight_limit: None, + proof_size_base_cost: None, } } } @@ -290,6 +311,8 @@ mod tests { max_fee_per_gas, max_priority_fee_per_gas, value, + weight_limit, + proof_size_base_cost, } = input; CheckEvmTransaction::::new( CheckEvmTransactionConfig { @@ -311,6 +334,8 @@ mod tests { value, access_list: vec![], }, + weight_limit, + proof_size_base_cost, ) } @@ -332,6 +357,16 @@ mod tests { test_env(input) } + fn transaction_gas_limit_low_proof_size<'config>( + is_transactional: bool, + ) -> CheckEvmTransaction<'config, TestError> { + let mut input = TestCase::default(); + input.weight_limit = Some(Weight::from_parts(1, 1)); + input.proof_size_base_cost = Some(2); + input.is_transactional = is_transactional; + test_env(input) + } + fn transaction_gas_limit_high<'config>() -> CheckEvmTransaction<'config, TestError> { let mut input = TestCase::default(); input.blockchain_gas_limit = U256::from(1u8); @@ -500,6 +535,42 @@ mod tests { assert!(res.is_ok()); } + #[test] + // Gas limit too low for proof size recording transactional fails in pool and in block. + fn validate_in_pool_and_block_transactional_fails_gas_limit_too_low_proof_size() { + let who = Account { + balance: U256::from(1_000_000u128), + nonce: U256::zero(), + }; + let is_transactional = true; + let test = transaction_gas_limit_low_proof_size(is_transactional); + // Pool + let res = test.validate_in_pool_for(&who); + assert!(res.is_err()); + assert_eq!(res.unwrap_err(), TestError::GasLimitTooLow); + // Block + let res = test.validate_in_block_for(&who); + assert!(res.is_err()); + assert_eq!(res.unwrap_err(), TestError::GasLimitTooLow); + } + + #[test] + // Gas limit too low non-transactional succeeds in pool and in block. + fn validate_in_pool_and_block_non_transactional_succeeds_gas_limit_too_low_proof_size() { + let who = Account { + balance: U256::from(1_000_000u128), + nonce: U256::zero(), + }; + let is_transactional = false; + let test = transaction_gas_limit_low_proof_size(is_transactional); + // Pool + let res = test.validate_in_pool_for(&who); + assert!(res.is_ok()); + // Block + let res = test.validate_in_block_for(&who); + assert!(res.is_ok()); + } + #[test] // Gas limit too high fails in pool and in block. fn validate_in_pool_for_fails_gas_limit_too_high() { diff --git a/primitives/rpc/src/lib.rs b/primitives/rpc/src/lib.rs index e00d447dd1..1ea4db3460 100644 --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -79,7 +79,7 @@ impl RuntimeStorageOverride for () { sp_api::decl_runtime_apis! { /// API necessary for Ethereum-compatibility layer. - #[api_version(4)] + #[api_version(5)] pub trait EthereumRuntimeRPCApi { /// Returns runtime defined pallet_evm::ChainId. fn chain_id() -> u64; @@ -104,7 +104,7 @@ sp_api::decl_runtime_apis! { gas_price: Option, nonce: Option, estimate: bool, - ) -> Result; + ) -> Result>, sp_runtime::DispatchError>; #[changed_in(4)] fn call( from: H160, @@ -116,7 +116,8 @@ sp_api::decl_runtime_apis! { max_priority_fee_per_gas: Option, nonce: Option, estimate: bool, - ) -> Result; + ) -> Result>, sp_runtime::DispatchError>; + #[changed_in(5)] fn call( from: H160, to: H160, @@ -128,7 +129,19 @@ sp_api::decl_runtime_apis! { nonce: Option, estimate: bool, access_list: Option)>>, - ) -> Result; + ) -> Result>, sp_runtime::DispatchError>; + fn call( + from: H160, + to: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + estimate: bool, + access_list: Option)>>, + ) -> Result>, sp_runtime::DispatchError>; /// Returns a frame_ethereum::create response. #[changed_in(2)] fn create( @@ -139,7 +152,7 @@ sp_api::decl_runtime_apis! { gas_price: Option, nonce: Option, estimate: bool, - ) -> Result; + ) -> Result, sp_runtime::DispatchError>; #[changed_in(4)] fn create( from: H160, @@ -150,7 +163,19 @@ sp_api::decl_runtime_apis! { max_priority_fee_per_gas: Option, nonce: Option, estimate: bool, - ) -> Result; + ) -> Result, sp_runtime::DispatchError>; + #[changed_in(5)] + fn create( + from: H160, + data: Vec, + value: U256, + gas_limit: U256, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + nonce: Option, + estimate: bool, + access_list: Option)>>, + ) -> Result, sp_runtime::DispatchError>; fn create( from: H160, data: Vec, @@ -161,7 +186,7 @@ sp_api::decl_runtime_apis! { nonce: Option, estimate: bool, access_list: Option)>>, - ) -> Result; + ) -> Result, sp_runtime::DispatchError>; /// Return the current block. Legacy. #[changed_in(2)] fn current_block() -> Option; diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index 59981a59a2..947c00df27 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -311,9 +311,11 @@ impl> FindAuthor for FindAuthorTruncated { } const BLOCK_GAS_LIMIT: u64 = 75_000_000; +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); + pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); pub PrecompilesValue: FrontierPrecompiles = FrontierPrecompiles::<_>::new(); pub WeightPerGas: Weight = Weight::from_parts(weight_per_gas(BLOCK_GAS_LIMIT, NORMAL_DISPATCH_RATIO, WEIGHT_MILLISECS_PER_BLOCK), 0); } @@ -336,6 +338,7 @@ impl pallet_evm::Config for Runtime { type OnChargeTransaction = (); type OnCreate = (); type FindAuthor = FindAuthorTruncated; + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type Timestamp = Timestamp; type WeightInfo = pallet_evm::weights::SubstrateWeight; } @@ -680,6 +683,9 @@ impl_runtime_apis! { access_list.unwrap_or_default(), is_transactional, validate, + // TODO we probably want to support external cost recording in non-transactional calls + None, + None, evm_config, ).map_err(|err| err.error.into()) } @@ -717,6 +723,9 @@ impl_runtime_apis! { access_list.unwrap_or_default(), is_transactional, validate, + // TODO we probably want to support external cost recording in non-transactional calls + None, + None, evm_config, ).map_err(|err| err.error.into()) }