diff --git a/Cargo.lock b/Cargo.lock index 77d720040b..b297ae470c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2328,20 +2328,20 @@ dependencies = [ [[package]] name = "ethereum" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89fb87a9e103f71b903b80b670200b54cc67a07578f070681f1fffb7396fb7" +checksum = "2e04d24d20b8ff2235cffbf242d5092de3aa45f77c5270ddbfadd2778ca13fea" dependencies = [ "bytes", "ethereum-types", - "hash-db 0.15.2", + "hash-db", "hash256-std-hasher", "parity-scale-codec", "rlp", "scale-info", "serde", "sha3", - "triehash", + "trie-root", ] [[package]] @@ -2368,8 +2368,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "evm" -version = "0.39.1" -source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01dea39965418e1a072fbe9e1651bd726f0e617611c0f24ab96613425272d1" dependencies = [ "auto_impl", "environmental", @@ -2388,8 +2389,9 @@ dependencies = [ [[package]] name = "evm-core" -version = "0.39.0" -source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da6cedc5cedb4208e59467106db0d1f50db01b920920589f8e672c02fdc04f" dependencies = [ "parity-scale-codec", "primitive-types", @@ -2399,8 +2401,9 @@ dependencies = [ [[package]] name = "evm-gasometer" -version = "0.39.0" -source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc0eb591abc5cd7b05bef6a036c2bb6c66ab6c5e0c5ce94bfe377ab670b1fd7" dependencies = [ "environmental", "evm-core", @@ -2410,8 +2413,9 @@ dependencies = [ [[package]] name = "evm-runtime" -version = "0.39.0" -source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bbe09b64ae13a29514048c1bb6fda6374ac0b4f6a1f15a443348ab88ef42cd" dependencies = [ "auto_impl", "environmental", @@ -3630,12 +3634,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "hash-db" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" - [[package]] name = "hash-db" version = "0.16.0" @@ -5116,7 +5114,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808b50db46293432a45e63bc15ea51e0ab4c0a1647b8eb114e31a3e698dd6fbe" dependencies = [ - "hash-db 0.16.0", + "hash-db", ] [[package]] @@ -5813,7 +5811,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "hash-db 0.16.0", + "hash-db", "hex", "hex-literal", "impl-trait-for-tuples", @@ -7566,7 +7564,7 @@ name = "sc-client-db" version = "0.10.0-dev" source = "git+https://github.com/paritytech/polkadot-sdk?branch=master#dc28df0b278f2ca0f69d14363e08668a9ecc2fac" dependencies = [ - "hash-db 0.16.0", + "hash-db", "kvdb", "kvdb-memorydb", "kvdb-rocksdb", @@ -8874,7 +8872,7 @@ name = "sp-api" version = "4.0.0-dev" source = "git+https://github.com/paritytech/polkadot-sdk?branch=master#dc28df0b278f2ca0f69d14363e08668a9ecc2fac" dependencies = [ - "hash-db 0.16.0", + "hash-db", "log", "parity-scale-codec", "scale-info", @@ -9056,7 +9054,7 @@ dependencies = [ "dyn-clonable", "ed25519-zebra", "futures", - "hash-db 0.16.0", + "hash-db", "hash256-std-hasher", "impl-serde", "lazy_static", @@ -9349,7 +9347,7 @@ name = "sp-state-machine" version = "0.28.0" source = "git+https://github.com/paritytech/polkadot-sdk?branch=master#dc28df0b278f2ca0f69d14363e08668a9ecc2fac" dependencies = [ - "hash-db 0.16.0", + "hash-db", "log", "parity-scale-codec", "parking_lot 0.12.1", @@ -9462,7 +9460,7 @@ version = "22.0.0" source = "git+https://github.com/paritytech/polkadot-sdk?branch=master#dc28df0b278f2ca0f69d14363e08668a9ecc2fac" dependencies = [ "ahash 0.8.3", - "hash-db 0.16.0", + "hash-db", "hashbrown 0.13.2", "lazy_static", "memory-db", @@ -10451,7 +10449,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "767abe6ffed88a1889671a102c2861ae742726f52e0a5a425b92c9fbfa7e9c85" dependencies = [ - "hash-db 0.16.0", + "hash-db", "hashbrown 0.13.2", "log", "rustc-hex", @@ -10464,17 +10462,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4ed310ef5ab98f5fa467900ed906cb9232dd5376597e00fd4cba2a449d06c0b" dependencies = [ - "hash-db 0.16.0", -] - -[[package]] -name = "triehash" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" -dependencies = [ - "hash-db 0.15.2", - "rlp", + "hash-db", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ec009253c7..d7bdb23e45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,9 +48,9 @@ bn = { package = "substrate-bn", version = "0.6", default-features = false } clap = { version = "4.4.3", features = ["derive", "deprecated"] } derive_more = "0.99" environmental = { version = "1.1.4", default-features = false } -ethereum = { version = "0.14.0", default-features = false } +ethereum = { version = "0.15.0", default-features = false } ethereum-types = { version = "0.14.1", default-features = false } -evm = { git = "https://github.com/rust-blockchain/evm", rev = "b7b82c7e1fc57b7449d6dfa6826600de37cc1e65", default-features = false } +evm = { version = "0.41.0", default-features = false } futures = "0.3.28" hash-db = { version = "0.16.0", default-features = false } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/frame/ethereum/src/mock.rs b/frame/ethereum/src/mock.rs index 9eeb8f7e7b..232a8eac06 100644 --- a/frame/ethereum/src/mock.rs +++ b/frame/ethereum/src/mock.rs @@ -132,13 +132,15 @@ impl FindAuthor for FindAuthorTruncated { 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 = 800 * 1024; parameter_types! { pub const TransactionByteFee: u64 = 1; pub const ChainId: u64 = 42; pub const EVMModuleId: PalletId = PalletId(*b"py/evmpa"); 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 const WeightPerGas: Weight = Weight::from_parts(20_000, 0); } @@ -174,6 +176,7 @@ impl pallet_evm::Config for Test { type OnCreate = (); type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; type Timestamp = Timestamp; type WeightInfo = (); diff --git a/frame/evm/precompile/dispatch/src/lib.rs b/frame/evm/precompile/dispatch/src/lib.rs index 4de25ae878..715831badf 100644 --- a/frame/evm/precompile/dispatch/src/lib.rs +++ b/frame/evm/precompile/dispatch/src/lib.rs @@ -85,8 +85,11 @@ where return Err(err); } - handle - .record_external_cost(Some(info.weight.ref_time()), Some(info.weight.proof_size()))?; + handle.record_external_cost( + Some(info.weight.ref_time()), + Some(info.weight.proof_size()), + None, + )?; match call.dispatch(Some(origin).into()) { Ok(post_info) => { diff --git a/frame/evm/precompile/dispatch/src/mock.rs b/frame/evm/precompile/dispatch/src/mock.rs index 1c55895817..64353cb5bb 100644 --- a/frame/evm/precompile/dispatch/src/mock.rs +++ b/frame/evm/precompile/dispatch/src/mock.rs @@ -155,6 +155,7 @@ impl pallet_evm::Config for Test { type FindAuthor = FindAuthorTruncated; type SuicideQuickClearLimit = SuicideQuickClearLimit; type GasLimitPovSizeRatio = (); + type GasLimitStorageGrowthRatio = (); type Timestamp = Timestamp; type WeightInfo = (); } @@ -185,6 +186,7 @@ impl PrecompileHandle for MockHandle { &mut self, _ref_time: Option, _proof_size: Option, + _storage_growth: Option, ) -> Result<(), ExitError> { Ok(()) } diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index 59945a4fd1..0f7519e0dc 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -172,6 +172,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. type Timestamp: Time; diff --git a/frame/evm/src/mock.rs b/frame/evm/src/mock.rs index 9e1e481d5d..977ba29b76 100644 --- a/frame/evm/src/mock.rs +++ b/frame/evm/src/mock.rs @@ -122,10 +122,13 @@ impl FindAuthor for FindAuthorTruncated { } 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 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 MockPrecompiles: MockPrecompileSet = MockPrecompileSet; pub SuicideQuickClearLimit: u32 = 0; @@ -153,6 +156,7 @@ impl crate::Config for Test { type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; + type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; 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..5ad50cd025 --- /dev/null +++ b/frame/evm/src/runner/meter.rs @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// This file is part of Frontier. +// +// Copyright (c) 2021-2022 Parity Technologies (UK) Ltd. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use evm::{ + gasometer::{GasCost, StorageTarget}, + Opcode, +}; +use fp_evm::ACCOUNT_STORAGE_PROOF_SIZE; +use sp_core::{H160, H256}; +use sp_std::collections::btree_set::BTreeSet; + +/// 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 bda922d39c..3a95a3d763 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 0850db17b1..72974d5a94 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -43,10 +43,12 @@ use sp_std::{ // 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, AccountStorages, AddressMapping, BalanceOf, BlockHashMapping, Config, Error, Event, FeeCalculator, OnChargeEVMTransaction, @@ -242,24 +244,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(sp_std::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(sp_std::cmp::max( + sp_std::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); @@ -681,6 +701,7 @@ pub struct SubstrateStackState<'vicinity, 'config, T> { original_storage: BTreeMap<(H160, H256), H256>, recorded: Recorded, weight_info: Option, + storage_meter: Option, _marker: PhantomData, } @@ -690,7 +711,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 { @@ -703,6 +726,7 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> { original_storage: BTreeMap::new(), recorded: Default::default(), weight_info, + storage_meter, } } @@ -1001,8 +1025,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)?; + } } }; } @@ -1012,9 +1046,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) => { @@ -1140,6 +1180,7 @@ where &mut self, ref_time: Option, proof_size: Option, + storage_growth: Option, ) -> Result<(), ExitError> { let weight_info = if let (Some(weight_info), _) = self.info_mut() { weight_info @@ -1153,6 +1194,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 6c4c294b90..d858df293c 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -624,6 +624,240 @@ 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 + } + + // Verifies that contract deployment fails when the necessary storage growth gas isn't + // provided, even if the gas limit surpasses the standard gas usage measured by the + // gasometer. + #[test] + fn contract_deployment_should_fail_oog() { + new_test_ext().execute_with(|| { + let gas_limit: u64 = 80_000; + + let result = create_test_contract(PROOF_SIZE_TEST_CALLEE_CONTRACT_BYTECODE, gas_limit) + .expect("create succeeds"); + + // Assert that the legacy gas is lower than the gas limit. + assert!(result.used_gas.standard < U256::from(gas_limit)); + assert_eq!( + result.exit_reason, + crate::ExitReason::Error(crate::ExitError::OutOfGas) + ); + assert_eq!(result.used_gas.effective.as_u64(), 78485); + // Assert that the contract entry does not exists in the storage. + assert!(!AccountCodes::::contains_key(result.value)); + }); + } + + /// 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/frame/evm/test-vector-support/src/lib.rs b/frame/evm/test-vector-support/src/lib.rs index dfc14f051c..5dd816b3e2 100644 --- a/frame/evm/test-vector-support/src/lib.rs +++ b/frame/evm/test-vector-support/src/lib.rs @@ -80,7 +80,12 @@ impl PrecompileHandle for MockHandle { Ok(()) } - fn record_external_cost(&mut self, _: Option, _: Option) -> Result<(), ExitError> { + fn record_external_cost( + &mut self, + _: Option, + _: Option, + _: Option, + ) -> Result<(), ExitError> { Ok(()) } diff --git a/precompiles/src/evm/handle.rs b/precompiles/src/evm/handle.rs index d3d26bbe57..74254025f6 100644 --- a/precompiles/src/evm/handle.rs +++ b/precompiles/src/evm/handle.rs @@ -60,7 +60,7 @@ impl PrecompileHandleExt for T { ) -> Result<(), evm::ExitError> { self.record_cost(crate::prelude::RuntimeHelper::::db_read_gas_cost())?; // TODO: record ref time when precompile will be benchmarked - self.record_external_cost(None, Some(data_max_encoded_len as u64)) + self.record_external_cost(None, Some(data_max_encoded_len as u64), None) } /// Record cost of a log manualy. @@ -190,6 +190,7 @@ mod tests { &mut self, _ref_time: Option, _proof_size: Option, + _storage_growth: Option, ) -> Result<(), fp_evm::ExitError> { Ok(()) } diff --git a/precompiles/src/precompile_set.rs b/precompiles/src/precompile_set.rs index 2f9bb0816a..ebe2d11cfd 100644 --- a/precompiles/src/precompile_set.rs +++ b/precompiles/src/precompile_set.rs @@ -477,8 +477,10 @@ impl<'a, H: PrecompileHandle> PrecompileHandle for RestrictiveHandle<'a, H> { &mut self, ref_time: Option, proof_size: Option, + storage_growth: Option, ) -> Result<(), ExitError> { - self.handle.record_external_cost(ref_time, proof_size) + self.handle + .record_external_cost(ref_time, proof_size, storage_growth) } fn refund_external_cost(&mut self, ref_time: Option, proof_size: Option) { diff --git a/precompiles/src/substrate.rs b/precompiles/src/substrate.rs index a5604af160..5ddeb69427 100644 --- a/precompiles/src/substrate.rs +++ b/precompiles/src/substrate.rs @@ -64,9 +64,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(); @@ -77,7 +78,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())) + handle.record_external_cost(None, Some(weight.proof_size()), Some(storage_growth)) } #[inline(always)] @@ -107,6 +108,7 @@ where handle: &mut impl PrecompileHandle, origin: ::RuntimeOrigin, call: Call, + storage_growth: u64, ) -> Result where Runtime::RuntimeCall: From, @@ -114,7 +116,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/src/testing/handle.rs b/precompiles/src/testing/handle.rs index f521954034..0e869700f2 100644 --- a/precompiles/src/testing/handle.rs +++ b/precompiles/src/testing/handle.rs @@ -208,6 +208,7 @@ impl PrecompileHandle for MockHandle { &mut self, _ref_time: Option, _proof_size: Option, + _storage_growth: Option, ) -> Result<(), ExitError> { Ok(()) } diff --git a/precompiles/tests-external/lib.rs b/precompiles/tests-external/lib.rs index 75199c1cee..f1ac618989 100644 --- a/precompiles/tests-external/lib.rs +++ b/precompiles/tests-external/lib.rs @@ -162,6 +162,7 @@ impl PrecompileHandle for MockPrecompileHandle { &mut self, _ref_time: Option, _proof_size: Option, + _storage_growth: Option, ) -> Result<(), fp_evm::ExitError> { Ok(()) } @@ -242,6 +243,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 ca0b59466a..f46424afe5 100644 --- a/primitives/evm/src/lib.rs +++ b/primitives/evm/src/lib.rs @@ -59,7 +59,7 @@ pub struct Vicinity { /// `System::Account` 16(hash) + 20 (key) + 60 (AccountInfo::max_encoded_len) pub const ACCOUNT_BASIC_PROOF_SIZE: u64 = 96; -/// `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; @@ -67,6 +67,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 5995ec780a..1196d11c59 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -318,10 +318,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; @@ -347,6 +350,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; }