diff --git a/frame/ethereum/src/mock.rs b/frame/ethereum/src/mock.rs index 5f711ec048..d14c95e211 100644 --- a/frame/ethereum/src/mock.rs +++ b/frame/ethereum/src/mock.rs @@ -85,6 +85,7 @@ impl FindAuthor for FindAuthorTruncated { parameter_types! { pub const TransactionByteFee: u64 = 1; + pub const GasLimitStorageGrowthRatio: u64 = 0; } #[derive_impl(pallet_evm::config_preludes::TestDefaultConfig)] @@ -96,6 +97,7 @@ impl pallet_evm::Config for Test { type PrecompilesValue = (); type Runner = pallet_evm::runner::stack::Runner; type FindAuthor = FindAuthorTruncated; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; } diff --git a/frame/evm/precompile/dispatch/src/mock.rs b/frame/evm/precompile/dispatch/src/mock.rs index 7a921371a8..98d21c8978 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 FindAuthor = FindAuthorTruncated; type SuicideQuickClearLimit = SuicideQuickClearLimit; type GasLimitPovSizeRatio = (); + type GasLimitStorageGrowthRatio = (); type Timestamp = Timestamp; type WeightInfo = (); } diff --git a/frame/evm/precompile/storage-cleaner/src/mock.rs b/frame/evm/precompile/storage-cleaner/src/mock.rs index 2fe0c16a6a..a63da6e9c0 100644 --- a/frame/evm/precompile/storage-cleaner/src/mock.rs +++ b/frame/evm/precompile/storage-cleaner/src/mock.rs @@ -114,10 +114,12 @@ pub type PCall = StorageCleanerPrecompileCall; const BLOCK_GAS_LIMIT: u64 = 15_000_000; const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +const MAX_STORAGE_GROWTH: u64 = 400 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); + pub const GasLimitStorageGrowthRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_STORAGE_GROWTH); pub WeightPerGas: Weight = Weight::from_parts(20_000, 0); pub PrecompilesValue: Precompiles = Precompiles::new(); pub SuicideQuickClearLimit: u32 = 0; @@ -143,6 +145,7 @@ impl pallet_evm::Config for Runtime { type FindAuthor = (); type OnCreate = (); type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; type WeightInfo = (); type SuicideQuickClearLimit = SuicideQuickClearLimit; diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index f94e0ffc18..b3689c4f81 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -195,6 +195,9 @@ pub mod pallet { /// Define the quick clear limit of storage clearing when a contract suicides. Set to 0 to disable it. type SuicideQuickClearLimit: Get; + /// Gas limit storage growth ratio. + type GasLimitStorageGrowthRatio: Get; + /// Get the timestamp for the current block. #[pallet::no_default] type Timestamp: Time; @@ -224,11 +227,14 @@ pub mod pallet { const BLOCK_GAS_LIMIT: u64 = 150_000_000; const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + /// The maximum storage growth per block in bytes. + const MAX_STORAGE_GROWTH: u64 = 400 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); pub const ChainId: u64 = 42; pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); + pub const GasLimitStorageGrowthRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_STORAGE_GROWTH); pub WeightPerGas: Weight = Weight::from_parts(20_000, 0); pub SuicideQuickClearLimit: u32 = 0; } @@ -251,6 +257,7 @@ pub mod pallet { type OnCreate = (); type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; type WeightInfo = (); } diff --git a/frame/evm/src/res/StorageGrowthTest.sol b/frame/evm/src/res/StorageGrowthTest.sol new file mode 100644 index 0000000000..7a66d33369 --- /dev/null +++ b/frame/evm/src/res/StorageGrowthTest.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.2; + +contract StorageGrowthTest { + mapping(uint256 => uint256) public map; + uint256 foo; + uint256 bar; + uint256 baz; + + constructor() { + foo = 1; + bar = 2; + baz = 3; + } + + function store() public { + map[0] = 1; + map[1] = 2; + map[2] = 3; + } + + function update() public { + foo = 2; + bar = 3; + baz = 4; + } +} diff --git a/frame/evm/src/res/storage_growth_test_contract_bytecode.txt b/frame/evm/src/res/storage_growth_test_contract_bytecode.txt new file mode 100644 index 0000000000..e0ba9113b0 --- /dev/null +++ b/frame/evm/src/res/storage_growth_test_contract_bytecode.txt @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506001808190555060028081905550600380819055506101c7806100356000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063975057e714610046578063a2e6204514610050578063b8dda9c71461005a575b600080fd5b61004e61008a565b005b6100586100d6565b005b610074600480360381019061006f919061011d565b6100f0565b6040516100819190610155565b60405180910390f35b6001600080808152602001908152602001600020819055506002600080600181526020019081526020016000208190555060036000806002815260200190815260200160002081905550565b600260018190555060036002819055506004600381905550565b60006020528060005260406000206000915090505481565b6000813590506101178161017a565b92915050565b60006020828403121561012f57600080fd5b600061013d84828501610108565b91505092915050565b61014f81610170565b82525050565b600060208201905061016a6000830184610146565b92915050565b6000819050919050565b61018381610170565b811461018e57600080fd5b5056fea2646970667358221220b25685afab962e465f0b43f6c4de10c82a565f50f6995c1cd444b002bcdd43e964736f6c63430008020033 \ No newline at end of file diff --git a/frame/evm/src/runner/meter.rs b/frame/evm/src/runner/meter.rs new file mode 100644 index 0000000000..374895a753 --- /dev/null +++ b/frame/evm/src/runner/meter.rs @@ -0,0 +1,209 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::collections::btree_set::BTreeSet; +use evm::{ + gasometer::{GasCost, StorageTarget}, + Opcode, +}; +use fp_evm::ACCOUNT_STORAGE_PROOF_SIZE; +use sp_core::{H160, H256}; + +/// An error that is returned when the storage limit has been exceeded. +#[derive(Debug, PartialEq)] +pub enum MeterError { + LimitExceeded, +} + +/// A meter for tracking the storage growth. +#[derive(Clone)] +pub struct StorageMeter { + usage: u64, + limit: u64, + recorded_new_entries: BTreeSet<(H160, H256)>, +} + +impl StorageMeter { + /// Creates a new storage meter with the given limit. + pub fn new(limit: u64) -> Self { + Self { + usage: 0, + limit, + recorded_new_entries: BTreeSet::new(), + } + } + + /// Records the given amount of storage usage. The amount is added to the current usage. + /// If the limit is reached, an error is returned. + pub fn record(&mut self, amount: u64) -> Result<(), MeterError> { + let usage = self + .usage + .checked_add(amount) + .ok_or(MeterError::LimitExceeded)?; + + if usage > self.limit { + return Err(MeterError::LimitExceeded); + } + self.usage = usage; + Ok(()) + } + + /// Records the storage growth for the given Opcode. + pub fn record_dynamic_opcode_cost( + &mut self, + _opcode: Opcode, + gas_cost: GasCost, + target: StorageTarget, + ) -> Result<(), MeterError> { + if let GasCost::SStore { original, new, .. } = gas_cost { + // Validate if storage growth for the current slot has been accounted for within this transaction. + // Comparing Original and new to determine if a new entry is being created is not sufficient, because + // 'original' updates only at the end of the transaction. So, if a new entry + // is created and updated multiple times within the same transaction, the storage growth is + // accounted for multiple times, because 'original' is always zero for the subsequent updates. + // To avoid this, we keep track of the new entries that are created within the transaction. + let (address, index) = match target { + StorageTarget::Slot(address, index) => (address, index), + _ => return Ok(()), + }; + let recorded = self.recorded_new_entries.contains(&(address, index)); + if !recorded && original == H256::default() && !new.is_zero() { + self.record(ACCOUNT_STORAGE_PROOF_SIZE)?; + self.recorded_new_entries.insert((address, index)); + } + } + Ok(()) + } + + /// Returns the current usage of storage. + pub fn usage(&self) -> u64 { + self.usage + } + + /// Returns the limit of storage. + pub fn limit(&self) -> u64 { + self.limit + } + + /// Returns the amount of storage that is available before the limit is reached. + pub fn available(&self) -> u64 { + self.limit.saturating_sub(self.usage) + } + + /// Map storage usage to the gas cost. + pub fn storage_to_gas(&self, ratio: u64) -> u64 { + self.usage.saturating_mul(ratio) + } +} +#[cfg(test)] +mod test { + use super::*; + + /// Tests the basic functionality of StorageMeter. + #[test] + fn test_basic_functionality() { + let limit = 100; + let mut meter = StorageMeter::new(limit); + + assert_eq!(meter.usage(), 0); + assert_eq!(meter.limit(), limit); + + let amount = 10; + meter.record(amount).unwrap(); + assert_eq!(meter.usage(), amount); + } + + /// Tests the behavior of StorageMeter when reaching the limit. + #[test] + fn test_reaching_limit() { + let limit = 100; + let mut meter = StorageMeter::new(limit); + + // Approaching the limit without exceeding + meter.record(limit - 1).unwrap(); + assert_eq!(meter.usage(), limit - 1); + + // Reaching the limit exactly + meter.record(1).unwrap(); + assert_eq!(meter.usage(), limit); + + // Exceeding the limit + let res = meter.record(1); + assert_eq!(meter.usage(), limit); + assert!(res.is_err()); + assert_eq!(res, Err(MeterError::LimitExceeded)); + } + + /// Tests the record of dynamic opcode cost. + #[test] + fn test_record_dynamic_opcode_cost() { + let limit = 200; + let mut meter = StorageMeter::new(limit); + + // Existing storage entry is updated. No change in storage growth. + let gas_cost = GasCost::SStore { + original: H256::from_low_u64_be(1), + current: Default::default(), + new: H256::from_low_u64_be(2), + target_is_cold: false, + }; + let target = StorageTarget::Slot(H160::default(), H256::from_low_u64_be(1)); + + meter + .record_dynamic_opcode_cost(Opcode::SSTORE, gas_cost, target) + .unwrap(); + assert_eq!(meter.usage(), 0); + + // New storage entry is created. Storage growth is recorded. + let gas_cost = GasCost::SStore { + original: H256::default(), + current: Default::default(), + new: H256::from_low_u64_be(1), + target_is_cold: false, + }; + meter + .record_dynamic_opcode_cost(Opcode::SSTORE, gas_cost, target) + .unwrap(); + assert_eq!(meter.usage(), ACCOUNT_STORAGE_PROOF_SIZE); + + // Try to record the same storage growth again. No change in storage growth. + let gas_cost = GasCost::SStore { + original: H256::default(), + current: Default::default(), + new: H256::from_low_u64_be(1), + target_is_cold: false, + }; + meter + .record_dynamic_opcode_cost(Opcode::SSTORE, gas_cost, target) + .unwrap(); + assert_eq!(meter.usage(), ACCOUNT_STORAGE_PROOF_SIZE); + + // New storage entry is created. Storage growth is recorded. The limit is reached. + let gas_cost = GasCost::SStore { + original: H256::default(), + current: Default::default(), + new: H256::from_low_u64_be(2), + target_is_cold: false, + }; + let target = StorageTarget::Slot(H160::default(), H256::from_low_u64_be(2)); + + let res = meter.record_dynamic_opcode_cost(Opcode::SSTORE, gas_cost, target); + assert!(res.is_err()); + assert_eq!(res, Err(MeterError::LimitExceeded)); + assert_eq!(meter.usage(), 116); + } +} diff --git a/frame/evm/src/runner/mod.rs b/frame/evm/src/runner/mod.rs index 45ed14299e..540bf46afc 100644 --- a/frame/evm/src/runner/mod.rs +++ b/frame/evm/src/runner/mod.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod meter; pub mod stack; use crate::{Config, Weight}; diff --git a/frame/evm/src/runner/stack.rs b/frame/evm/src/runner/stack.rs index 3546b45d31..613c3c1d93 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -42,10 +42,12 @@ use sp_runtime::traits::UniqueSaturatedInto; // Frontier use fp_evm::{ 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, + Vicinity, WeightInfo, ACCOUNT_BASIC_PROOF_SIZE, ACCOUNT_CODES_KEY_SIZE, + ACCOUNT_CODES_METADATA_PROOF_SIZE, ACCOUNT_STORAGE_PROOF_SIZE, IS_EMPTY_CHECK_PROOF_SIZE, + WRITE_PROOF_SIZE, }; +use super::meter::StorageMeter; use crate::{ runner::Runner as RunnerT, AccountCodes, AccountCodesMetadata, AccountProvider, AccountStorages, AddressMapping, BalanceOf, BlockHashMapping, Config, Error, Event, @@ -239,24 +241,42 @@ where origin: source, }; + // Compute the storage limit based on the gas limit and the storage growth ratio. + let storage_growth_ratio = T::GasLimitStorageGrowthRatio::get(); + let storage_limit = if storage_growth_ratio > 0 { + let storage_limit = gas_limit.saturating_div(storage_growth_ratio); + Some(storage_limit) + } else { + None + }; + let metadata = StackSubstateMetadata::new(gas_limit, config); - let state = SubstrateStackState::new(&vicinity, metadata, maybe_weight_info); + let state = SubstrateStackState::new(&vicinity, metadata, maybe_weight_info, storage_limit); let mut executor = StackExecutor::new_with_precompiles(state, config, precompiles); let (reason, retv) = f(&mut executor); + // Compute the storage gas cost based on the storage growth. + let storage_gas = match &executor.state().storage_meter { + Some(storage_meter) => storage_meter.storage_to_gas(storage_growth_ratio), + None => 0, + }; + + let pov_gas = match executor.state().weight_info() { + Some(weight_info) => weight_info + .proof_size_usage + .unwrap_or_default() + .saturating_mul(T::GasLimitPovSizeRatio::get()), + None => 0, + }; + // Post execution. let used_gas = executor.used_gas(); - let effective_gas = match executor.state().weight_info() { - Some(weight_info) => U256::from(core::cmp::max( - used_gas, - weight_info - .proof_size_usage - .unwrap_or_default() - .saturating_mul(T::GasLimitPovSizeRatio::get()), - )), - _ => used_gas.into(), - }; + let effective_gas = U256::from(core::cmp::max( + core::cmp::max(used_gas, pov_gas), + storage_gas, + )); + let actual_fee = effective_gas.saturating_mul(total_fee_per_gas); let actual_base_fee = effective_gas.saturating_mul(base_fee); @@ -678,6 +698,7 @@ pub struct SubstrateStackState<'vicinity, 'config, T> { original_storage: BTreeMap<(H160, H256), H256>, recorded: Recorded, weight_info: Option, + storage_meter: Option, _marker: PhantomData, } @@ -687,7 +708,9 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> { vicinity: &'vicinity Vicinity, metadata: StackSubstateMetadata<'config>, weight_info: Option, + storage_limit: Option, ) -> Self { + let storage_meter = storage_limit.map(StorageMeter::new); Self { vicinity, substate: SubstrateStackSubstate { @@ -700,6 +723,7 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> { original_storage: BTreeMap::new(), recorded: Default::default(), weight_info, + storage_meter, } } @@ -998,8 +1022,18 @@ where ExternalOperation::IsEmpty => { weight_info.try_record_proof_size_or_fail(IS_EMPTY_CHECK_PROOF_SIZE)? } - ExternalOperation::Write(_) => { - weight_info.try_record_proof_size_or_fail(WRITE_PROOF_SIZE)? + ExternalOperation::Write(len) => { + weight_info.try_record_proof_size_or_fail(WRITE_PROOF_SIZE)?; + + if let Some(storage_meter) = self.storage_meter.as_mut() { + // Record the number of bytes written to storage when deploying a contract. + let storage_growth = ACCOUNT_CODES_KEY_SIZE + .saturating_add(ACCOUNT_CODES_METADATA_PROOF_SIZE) + .saturating_add(len.as_u64()); + storage_meter + .record(storage_growth) + .map_err(|_| ExitError::OutOfGas)?; + } } }; } @@ -1009,9 +1043,15 @@ where fn record_external_dynamic_opcode_cost( &mut self, opcode: Opcode, - _gas_cost: GasCost, + gas_cost: GasCost, target: evm::gasometer::StorageTarget, ) -> Result<(), ExitError> { + if let Some(storage_meter) = self.storage_meter.as_mut() { + storage_meter + .record_dynamic_opcode_cost(opcode, gas_cost, target) + .map_err(|_| ExitError::OutOfGas)?; + } + // If account code or storage slot is in the overlay it is already accounted for and early exit let accessed_storage: Option = match target { StorageTarget::Address(address) => { @@ -1137,7 +1177,7 @@ where &mut self, ref_time: Option, proof_size: Option, - _storage_growth: Option, + storage_growth: Option, ) -> Result<(), ExitError> { let weight_info = if let (Some(weight_info), _) = self.info_mut() { weight_info @@ -1151,6 +1191,13 @@ where if let Some(amount) = proof_size { weight_info.try_record_proof_size_or_fail(amount)?; } + if let Some(storage_meter) = self.storage_meter.as_mut() { + if let Some(amount) = storage_growth { + storage_meter + .record(amount) + .map_err(|_| ExitError::OutOfGas)?; + } + } Ok(()) } diff --git a/frame/evm/src/tests.rs b/frame/evm/src/tests.rs index 8bef720c77..07cd52131b 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -621,6 +621,217 @@ mod proof_size_test { } } +mod storage_growth_test { + use super::*; + use crate::tests::proof_size_test::PROOF_SIZE_TEST_CALLEE_CONTRACT_BYTECODE; + use fp_evm::{ + ACCOUNT_CODES_KEY_SIZE, ACCOUNT_CODES_METADATA_PROOF_SIZE, ACCOUNT_STORAGE_PROOF_SIZE, + }; + + const PROOF_SIZE_CALLEE_CONTRACT_BYTECODE_LEN: u64 = 116; + // The contract bytecode stored on chain. + const STORAGE_GROWTH_TEST_CONTRACT: &str = + include_str!("./res/storage_growth_test_contract_bytecode.txt"); + const STORAGE_GROWTH_TEST_CONTRACT_BYTECODE_LEN: u64 = 455; + + fn create_test_contract( + contract: &str, + gas_limit: u64, + ) -> Result>> { + ::Runner::create( + H160::default(), + hex::decode(contract.trim_end()).expect("Failed to decode contract"), + U256::zero(), + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + Some(FixedGasWeightMapping::::gas_to_weight( + gas_limit, true, + )), + Some(0), + ::config(), + ) + } + + // Calls the given contract + fn call_test_contract( + contract_addr: H160, + call_data: &[u8], + value: U256, + gas_limit: u64, + ) -> Result>> { + ::Runner::call( + H160::default(), + contract_addr, + call_data.to_vec(), + value, + gas_limit, + Some(FixedGasPrice::min_gas_price().0), + None, + None, + Vec::new(), + true, // transactional + true, // must be validated + None, + Some(0), + ::config(), + ) + } + + // Computes the expected gas for contract creation (related to storage growth). + // `byte_code_len` represents the length of the contract bytecode stored on-chain. + fn expected_contract_create_storage_growth_gas(bytecode_len: u64) -> u64 { + let ratio = <::GasLimitStorageGrowthRatio as Get>::get(); + (ACCOUNT_CODES_KEY_SIZE + ACCOUNT_CODES_METADATA_PROOF_SIZE + bytecode_len) * ratio + } + + /// Test that contract deployment succeeds when the necessary storage growth gas is provided. + #[test] + fn contract_deployment_should_succeed() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 85_000; + + let result = create_test_contract(PROOF_SIZE_TEST_CALLEE_CONTRACT_BYTECODE, gas_limit) + .expect("create succeeds"); + + assert_eq!( + result.used_gas.effective.as_u64(), + expected_contract_create_storage_growth_gas( + PROOF_SIZE_CALLEE_CONTRACT_BYTECODE_LEN + ) + ); + assert_eq!( + result.exit_reason, + crate::ExitReason::Succeed(ExitSucceed::Returned) + ); + // Assert that the contract entry exists in the storage. + assert!(AccountCodes::::contains_key(result.value)); + }); + } + + // Test that contract creation with code initialization that results in new storage entries + // succeeds when the necessary storage growth gas is provided. + #[test] + fn contract_creation_with_code_initialization_should_succeed() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 863_394; + let ratio = <::GasLimitStorageGrowthRatio as Get>::get(); + // The constructor of the contract creates 3 new storage entries (uint256). So, + // the expected gas is the gas for contract creation + 3 * ACCOUNT_STORAGE_PROOF_SIZE. + let expected_storage_growth_gas = expected_contract_create_storage_growth_gas( + STORAGE_GROWTH_TEST_CONTRACT_BYTECODE_LEN, + ) + (3 * ACCOUNT_STORAGE_PROOF_SIZE * ratio); + + // Deploy the contract. + let result = create_test_contract(STORAGE_GROWTH_TEST_CONTRACT, gas_limit) + .expect("create succeeds"); + + assert_eq!( + result.used_gas.effective.as_u64(), + expected_storage_growth_gas + ); + assert_eq!( + result.exit_reason, + crate::ExitReason::Succeed(ExitSucceed::Returned) + ); + }); + } + + // Verify that saving new entries fails when insufficient storage growth gas is supplied. + #[test] + fn store_new_entries_should_fail_oog() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 863_394; + // Deploy the contract. + let res = create_test_contract(STORAGE_GROWTH_TEST_CONTRACT, gas_limit) + .expect("create succeeds"); + let contract_addr = res.value; + + let gas_limit = 120_000; + // Call the contract method store to store new entries. + let result = call_test_contract( + contract_addr, + &hex::decode("975057e7").unwrap(), + U256::zero(), + gas_limit, + ) + .expect("call should succeed"); + + assert_eq!( + result.exit_reason, + crate::ExitReason::Error(crate::ExitError::OutOfGas) + ); + }); + } + + // Verify that saving new entries succeeds when sufficient storage growth gas is supplied. + #[test] + fn store_new_entries_should_succeeds() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 863_394; + // Deploy the contract. + let res = create_test_contract(STORAGE_GROWTH_TEST_CONTRACT, gas_limit) + .expect("create succeeds"); + let contract_addr = res.value; + + let gas_limit = 128_000; + // Call the contract method store to store new entries. + let result = call_test_contract( + contract_addr, + &hex::decode("975057e7").unwrap(), + U256::zero(), + gas_limit, + ) + .expect("call should succeed"); + + let expected_storage_growth_gas = 3 + * ACCOUNT_STORAGE_PROOF_SIZE + * <::GasLimitStorageGrowthRatio as Get>::get(); + assert_eq!( + result.exit_reason, + crate::ExitReason::Succeed(ExitSucceed::Stopped) + ); + assert_eq!( + result.used_gas.effective.as_u64(), + expected_storage_growth_gas + ); + }); + } + + // Verify that updating existing storage entries does not incur any storage growth charges. + #[test] + fn update_exisiting_entries_succeeds() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 863_394; + // Deploy the contract. + let res = create_test_contract(STORAGE_GROWTH_TEST_CONTRACT, gas_limit) + .expect("create succeeds"); + let contract_addr = res.value; + + // Providing gas limit of 37_000 is enough to update existing entries, but not enough + // to store new entries. + let gas_limit = 37_000; + // Call the contract method update to update existing entries. + let result = call_test_contract( + contract_addr, + &hex::decode("a2e62045").unwrap(), + U256::zero(), + gas_limit, + ) + .expect("call should succeed"); + + assert_eq!( + result.exit_reason, + crate::ExitReason::Succeed(ExitSucceed::Stopped) + ); + }); + } +} + type Balances = pallet_balances::Pallet; #[allow(clippy::upper_case_acronyms)] type EVM = Pallet; diff --git a/precompiles/src/substrate.rs b/precompiles/src/substrate.rs index 456ab154fb..cbe38768b1 100644 --- a/precompiles/src/substrate.rs +++ b/precompiles/src/substrate.rs @@ -63,9 +63,10 @@ where Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, { #[inline(always)] - pub fn record_weight_v2_cost( + pub fn record_external_cost( handle: &mut impl PrecompileHandle, weight: Weight, + storage_growth: u64, ) -> Result<(), ExitError> { // Make sure there is enough gas. let remaining_gas = handle.remaining_gas(); @@ -76,7 +77,7 @@ where // Make sure there is enough remaining weight // TODO: record ref time when precompile will be benchmarked - handle.record_external_cost(None, Some(weight.proof_size()), None) + handle.record_external_cost(None, Some(weight.proof_size()), Some(storage_growth)) } #[inline(always)] @@ -106,6 +107,7 @@ where handle: &mut impl PrecompileHandle, origin: ::RuntimeOrigin, call: Call, + storage_growth: u64, ) -> Result where Runtime::RuntimeCall: From, @@ -113,7 +115,8 @@ where let call = Runtime::RuntimeCall::from(call); let dispatch_info = call.get_dispatch_info(); - Self::record_weight_v2_cost(handle, dispatch_info.weight).map_err(TryDispatchError::Evm)?; + Self::record_external_cost(handle, dispatch_info.weight, storage_growth) + .map_err(TryDispatchError::Evm)?; // Dispatch call. // It may be possible to not record gas cost if the call returns Pays::No. diff --git a/precompiles/tests-external/lib.rs b/precompiles/tests-external/lib.rs index b79fed5cb4..fd5a18e4a5 100644 --- a/precompiles/tests-external/lib.rs +++ b/precompiles/tests-external/lib.rs @@ -250,6 +250,7 @@ impl pallet_evm::Config for Runtime { type FindAuthor = (); type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; + type GasLimitStorageGrowthRatio = (); type Timestamp = Timestamp; type WeightInfo = pallet_evm::weights::SubstrateWeight; } diff --git a/primitives/evm/src/lib.rs b/primitives/evm/src/lib.rs index 377b9fe2ae..aca1a0e68a 100644 --- a/primitives/evm/src/lib.rs +++ b/primitives/evm/src/lib.rs @@ -63,7 +63,7 @@ pub struct Vicinity { /// `System::Account` 16(hash) + 20 (key) + 72 (AccountInfo::max_encoded_len) pub const ACCOUNT_BASIC_PROOF_SIZE: u64 = 108; -/// `AccountCodesMetadata` read, temptatively 16 (hash) + 20 (key) + 40 (CodeMetadata). +/// `AccountCodesMetadata` read, temtatively 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; @@ -71,6 +71,8 @@ pub const ACCOUNT_STORAGE_PROOF_SIZE: u64 = 116; 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; +/// `AccountCodes` key size. 16 (hash) + 20 (key) +pub const ACCOUNT_CODES_KEY_SIZE: u64 = 36; pub enum AccessedStorage { AccountCodes(H160), diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index b2fa840edc..c7cac3d0a5 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -338,10 +338,13 @@ impl> FindAuthor for FindAuthorTruncated { const BLOCK_GAS_LIMIT: u64 = 75_000_000; const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +/// The maximum storage growth per block in bytes. +const MAX_STORAGE_GROWTH: u64 = 400 * 1024; parameter_types! { pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT); pub const GasLimitPovSizeRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_POV_SIZE); + pub const GasLimitStorageGrowthRatio: u64 = BLOCK_GAS_LIMIT.saturating_div(MAX_STORAGE_GROWTH); 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); pub SuicideQuickClearLimit: u32 = 0; @@ -368,6 +371,7 @@ impl pallet_evm::Config for Runtime { type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; type WeightInfo = pallet_evm::weights::SubstrateWeight; }