diff --git a/Cargo.lock b/Cargo.lock index 282ef30ed3a..5de54a00153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8059,19 +8059,25 @@ name = "pallet-evm-tracker" version = "0.1.0" dependencies = [ "domain-runtime-primitives", + "frame-benchmarking", "frame-support", "frame-system", + "pallet-balances", "pallet-block-fees", "pallet-ethereum", "pallet-evm", + "pallet-timestamp", "pallet-transaction-payment", + "pallet-utility", "parity-scale-codec", "scale-info", "sp-core", "sp-domains", "sp-evm-tracker", + "sp-io", "sp-runtime", "sp-weights", + "static_assertions", "subspace-runtime-primitives", ] diff --git a/crates/subspace-node/Cargo.toml b/crates/subspace-node/Cargo.toml index abbf997f9a5..02393f3833a 100644 --- a/crates/subspace-node/Cargo.toml +++ b/crates/subspace-node/Cargo.toml @@ -70,6 +70,7 @@ sp-keystore.workspace = true sp-messenger.workspace = true sp-runtime.workspace = true subspace-core-primitives.workspace = true +subspace-logging.workspace = true subspace-metrics.workspace = true subspace-networking.workspace = true subspace-proof-of-space.workspace = true @@ -82,7 +83,6 @@ thiserror.workspace = true tokio = { workspace = true, features = ["macros"] } tokio-stream.workspace = true tracing.workspace = true -subspace-logging.workspace = true [build-dependencies] substrate-build-script-utils.workspace = true diff --git a/domains/pallets/evm-tracker/Cargo.toml b/domains/pallets/evm-tracker/Cargo.toml index 6025b7b6dc8..19e33f4fbd5 100644 --- a/domains/pallets/evm-tracker/Cargo.toml +++ b/domains/pallets/evm-tracker/Cargo.toml @@ -15,36 +15,58 @@ include = [ [dependencies] parity-scale-codec = { workspace = true, features = ["derive"] } domain-runtime-primitives.workspace = true +frame-benchmarking = { workspace = true, optional = true } frame-support.workspace = true frame-system.workspace = true +pallet-block-fees.workspace = true pallet-ethereum.workspace = true pallet-evm.workspace = true -pallet-block-fees.workspace = true pallet-transaction-payment.workspace = true +pallet-utility = { workspace = true, optional = true } scale-info = { workspace = true, features = ["derive"] } sp-core.workspace = true sp-domains.workspace = true sp-evm-tracker.workspace = true sp-runtime.workspace = true sp-weights.workspace = true +static_assertions = { workspace = true, optional = true } subspace-runtime-primitives.workspace = true +[dev-dependencies] +frame-benchmarking.workspace = true +pallet-balances.workspace = true +pallet-timestamp.workspace = true +pallet-utility.workspace = true +sp-io.workspace = true + [features] default = ["std"] std = [ - "parity-scale-codec/std", "domain-runtime-primitives/std", + "frame-benchmarking?/std", "frame-support/std", "frame-system/std", + "pallet-balances/std", "pallet-ethereum/std", "pallet-evm/std", "pallet-block-fees/std", + "pallet-timestamp/std", "pallet-transaction-payment/std", + "pallet-utility?/std", + "parity-scale-codec/std", "scale-info/std", "sp-core/std", "sp-domains/std", "sp-evm-tracker/std", + "sp-io/std", "sp-runtime/std", "sp-weights/std", "subspace-runtime-primitives/std", ] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-utility/runtime-benchmarks", + "static_assertions", +] diff --git a/domains/pallets/evm-tracker/src/benchmarking.rs b/domains/pallets/evm-tracker/src/benchmarking.rs new file mode 100644 index 00000000000..d5525b4e29a --- /dev/null +++ b/domains/pallets/evm-tracker/src/benchmarking.rs @@ -0,0 +1,248 @@ +//! Benchmarking for `pallet-evm-tracker::check_contract` extension. + +use super::*; +use crate::create_contract::CheckContractCreation; +use crate::traits::{AccountIdFor, MaybeIntoEthCall, MaybeIntoEvmCall}; +use crate::{EthereumAccountId, MAXIMUM_NUMBER_OF_CALLS, PermissionedActionAllowedBy}; +use frame_benchmarking::v2::*; +use frame_support::pallet_prelude::DispatchClass; +use frame_system::pallet_prelude::{OriginFor, RuntimeCallFor}; +use pallet_evm::GasWeightMapping; +use scale_info::prelude::vec; +use scale_info::prelude::vec::Vec; +use sp_core::crypto::AccountId32; +use sp_core::{Get, H160}; +use sp_runtime::traits::AsSystemOriginSigner; +use static_assertions::const_assert; +use subspace_runtime_primitives::utility::MaybeNestedCall; + +/// The number of accounts to use in the benchmark allow list. +/// We deliberately use a large number to generate a realistic weight. +const ACCOUNT_COUNT: usize = 10; + +/// The number of calls we can recursively make without hitting the stack limit. +/// (Beyond this limit, the weight calculation code assumes a linear increase in weight.) +const MAXIMUM_NUMBER_OF_CALLS_ON_STACK: u32 = 1_300; + +const_assert!(MAXIMUM_NUMBER_OF_CALLS_ON_STACK <= MAXIMUM_NUMBER_OF_CALLS); + +#[allow(clippy::multiple_bound_locations)] +#[benchmarks( + where + T: frame_system::Config + pallet_utility::Config + + pallet_ethereum::Config + pallet_evm::Config + scale_info::TypeInfo + + core::fmt::Debug + core::marker::Send + core::marker::Sync, + RuntimeCallFor: MaybeIntoEthCall + MaybeIntoEvmCall + MaybeNestedCall + + From> + From>, + Result>: From>, + OriginFor: + AsSystemOriginSigner> + From> + Clone, + )] +mod benchmarks { + use super::*; + + #[benchmark] + fn evm_contract_check_multiple(c: Linear<0, MAXIMUM_NUMBER_OF_CALLS>) { + let account_ids = (0..ACCOUNT_COUNT) + .map(|i| account_id(i as u128)) + .collect::>(); + // Checking multiple accounts is more expensive than single or empty accounts, or Anyone. + let accounts = generate_evm_account_list(&account_ids, EvmAccountList::Multiple); + + ContractCreationAllowedBy::::put(accounts); + + // Checking a disallowed account is more expensive, because we have to check if the + // transaction contains any creates. + let sender = EthereumAccountId::from(account_id(ACCOUNT_COUNT as u128 + u128::from(c))); + let sender_origin = Some(sender).into(); + + let mut calls = Vec::with_capacity(c as usize + 1); + for _i in 0..=c { + let contract_call = generate_evm_domain_call::( + sender, + // Checking a non-contract call is more expensive, because we can't early exit. + // (We can only exit the check loop early if we encounter a contract call.) + false, + 0, + U256::default(), + U256::default(), + ); + + calls.push(contract_call); + } + + let call = construct_utility_call_list::(calls); + + #[block] + { + // Checking signed is more expensive, because it has to check the allow list. + let _ = CheckContractCreation::::do_validate_signed(&sender_origin, &call); + } + } + + #[benchmark] + fn evm_contract_check_nested(c: Linear<0, MAXIMUM_NUMBER_OF_CALLS_ON_STACK>) { + let account_ids = (0..ACCOUNT_COUNT) + .map(|i| account_id(i as u128)) + .collect::>(); + let accounts = generate_evm_account_list(&account_ids, EvmAccountList::Multiple); + + ContractCreationAllowedBy::::put(accounts); + + let sender = EthereumAccountId::from(account_id(ACCOUNT_COUNT as u128 + u128::from(c))); + let sender_origin = Some(sender).into(); + + let nested_call = generate_evm_domain_call::( + sender, + // In this case, the call type doesn't matter, because we have to check each nested call. + false, + c, + U256::default(), + U256::default(), + ); + + #[block] + { + let _ = CheckContractCreation::::do_validate_signed(&sender_origin, &nested_call); + } + } + + // TODO: impl_benchmark_test_suite!() aganst a mock runtime, when + // `cargo test --features=runtime-benchmarks` is fixed for existing benchmark tests. +} + +/// Returns an AccountId32 with a deterministic address. +pub fn account_id(seed: u128) -> AccountId32 { + let mut account_id = [0u8; 32]; + account_id[0..16].copy_from_slice(&seed.to_be_bytes()); + account_id[16..32].copy_from_slice(&seed.to_be_bytes()); + + AccountId32::from(Into::<[u8; 32]>::into(account_id)) +} + +/// The kind of account list to generate. +#[expect(dead_code)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EvmAccountList { + Anyone, + NoOne, + One, + Multiple, +} + +/// Generate the supplied kind of account list. +pub fn generate_evm_account_list( + account_ids: &[AccountId32], + account_list_type: EvmAccountList, +) -> PermissionedActionAllowedBy { + match account_list_type { + EvmAccountList::Anyone => PermissionedActionAllowedBy::Anyone, + EvmAccountList::NoOne => PermissionedActionAllowedBy::Accounts(Vec::new()), + EvmAccountList::One => { + PermissionedActionAllowedBy::Accounts(vec![EthereumAccountId::from( + account_ids[0].clone(), + )]) + } + EvmAccountList::Multiple => PermissionedActionAllowedBy::Accounts( + account_ids + .iter() + .map(|id| EthereumAccountId::from(id.clone())) + .collect::>(), + ), + } +} + +pub fn max_extrinsic_gas( + multiplier: u64, +) -> u64 { + let limits: frame_system::limits::BlockWeights = + ::BlockWeights::get(); + // `limits.get(DispatchClass::Normal).max_extrinsic` is too large to use as `gas_limit` + // thus use `base_extrinsic` + let max_extrinsic = limits.get(DispatchClass::Normal).base_extrinsic * multiplier; + + ::GasWeightMapping::weight_to_gas(max_extrinsic) +} + +/// Generate a pallet-evm call, which can be passed to `construct_and_send_extrinsic()`. +/// `use_create` determines whether to use `create`, `create2`, or a non-create call. +/// `recursion_depth` determines the number of `pallet_utility::Call` wrappers to use. +pub fn generate_evm_domain_call( + account_id: EthereumAccountId, + use_create: bool, + recursion_depth: u32, + nonce: U256, + gas_price: U256, +) -> RuntimeCallFor +where + TestRuntime: frame_system::Config + pallet_evm::Config + pallet_utility::Config, + RuntimeCallFor: + From> + From>, +{ + if recursion_depth > 0 { + let inner_call = generate_evm_domain_call::( + account_id, + use_create, + recursion_depth - 1, + nonce, + gas_price, + ); + + // TODO: + // - randomly choose from the 6 different utility wrapper calls + // - test this call as the second call in a batch + // - test __Ignore calls are ignored + return construct_utility_call::(inner_call); + } + + let call = if use_create { + // TODO: + // - randomly choose from Create or Create2 calls + pallet_evm::Call::::create { + source: account_id.into(), + init: vec![0; 100], + value: U256::zero(), + gas_limit: max_extrinsic_gas::(1000), + max_fee_per_gas: gas_price, + access_list: vec![], + max_priority_fee_per_gas: Some(U256::from(1)), + nonce: Some(nonce), + } + } else { + pallet_evm::Call::::call { + source: account_id.into(), + target: H160::default(), + input: vec![0; 100], + value: U256::zero(), + gas_limit: max_extrinsic_gas::(1000), + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: Some(U256::from(1)), + nonce: Some(nonce), + access_list: vec![], + } + }; + + call.into() +} + +fn construct_utility_call(call: RuntimeCallFor) -> RuntimeCallFor +where + RuntimeCallFor: From>, +{ + pallet_utility::Call::batch_all { + calls: vec![call.into()], + } + .into() +} + +fn construct_utility_call_list( + calls: Vec>, +) -> RuntimeCallFor +where + RuntimeCallFor: From>, +{ + pallet_utility::Call::batch_all { + calls: calls.into_iter().map(Into::into).collect(), + } + .into() +} diff --git a/domains/pallets/evm-tracker/src/create_contract.rs b/domains/pallets/evm-tracker/src/create_contract.rs index addae1300b6..d142e9e8c96 100644 --- a/domains/pallets/evm-tracker/src/create_contract.rs +++ b/domains/pallets/evm-tracker/src/create_contract.rs @@ -1,16 +1,19 @@ //! Contract creation allow list implementations use crate::traits::{AccountIdFor, MaybeIntoEthCall, MaybeIntoEvmCall}; +use crate::weights::SubstrateWeightInfo; +use crate::{MAXIMUM_NUMBER_OF_CALLS, WeightInfo}; use domain_runtime_primitives::{ERR_CONTRACT_CREATION_NOT_ALLOWED, EthereumAccountId}; -use frame_support::pallet_prelude::{PhantomData, TypeInfo}; +use frame_support::RuntimeDebugNoBound; +use frame_support::dispatch::PostDispatchInfo; +use frame_support::pallet_prelude::{DispatchResult, PhantomData, TypeInfo}; use frame_system::pallet_prelude::{OriginFor, RuntimeCallFor}; use pallet_ethereum::{Transaction as EthereumTransaction, TransactionAction}; use parity_scale_codec::{Decode, Encode}; use scale_info::prelude::fmt; -use sp_core::Get; -use sp_runtime::impl_tx_ext_default; use sp_runtime::traits::{ - AsSystemOriginSigner, DispatchInfoOf, Dispatchable, TransactionExtension, ValidateResult, + AsSystemOriginSigner, DispatchInfoOf, DispatchOriginOf, Dispatchable, PostDispatchInfoOf, + RefundWeight, TransactionExtension, ValidateResult, }; use sp_runtime::transaction_validity::{ InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, @@ -25,7 +28,7 @@ use subspace_runtime_primitives::utility::{MaybeNestedCall, nested_call_iter}; pub fn is_create_contract_allowed( call: &RuntimeCallFor, signer: &EthereumAccountId, -) -> bool +) -> (bool, u32) where Runtime: frame_system::Config + pallet_ethereum::Config @@ -37,14 +40,18 @@ where { // If the account is allowed to create contracts, or it's not a contract call, return true. // Only enters allocating code if this account can't create contracts. - crate::Pallet::::is_allowed_to_create_contracts(signer) - || !is_create_contract::(call) + if crate::Pallet::::is_allowed_to_create_contracts(signer) { + return (true, 0); + } + + let (is_create, call_count) = is_create_contract::(call); + (!is_create, call_count) } /// If anyone is allowed to create contracts, allows contracts. Otherwise, rejects contracts. /// Returns false if the call is a contract call, and there is a specific (possibly empty) allow /// list. Otherwise, returns true. -pub fn is_create_unsigned_contract_allowed(call: &RuntimeCallFor) -> bool +pub fn is_create_unsigned_contract_allowed(call: &RuntimeCallFor) -> (bool, u32) where Runtime: frame_system::Config + pallet_ethereum::Config + pallet_evm::Config + crate::Config, RuntimeCallFor: @@ -53,19 +60,26 @@ where { // If any account is allowed to create contracts, or it's not a contract call, return true. // Only enters allocating code if there is a contract creation filter. - crate::Pallet::::is_allowed_to_create_unsigned_contracts() - || !is_create_contract::(call) + if crate::Pallet::::is_allowed_to_create_unsigned_contracts() { + return (true, 0); + } + + let (is_create, call_count) = is_create_contract::(call); + (!is_create, call_count) } /// Returns true if the call is a contract creation call. -pub fn is_create_contract(call: &RuntimeCallFor) -> bool +pub fn is_create_contract(call: &RuntimeCallFor) -> (bool, u32) where Runtime: frame_system::Config + pallet_ethereum::Config + pallet_evm::Config, RuntimeCallFor: MaybeIntoEthCall + MaybeIntoEvmCall + MaybeNestedCall, Result>: From>, { + let mut call_count = 0; for call in nested_call_iter::(call) { + call_count += 1; + if let Some(call) = call.maybe_into_eth_call() { match call { pallet_ethereum::Call::transact { @@ -73,7 +87,7 @@ where .. } => { if transaction.action == TransactionAction::Create { - return true; + return (true, call_count); } } pallet_ethereum::Call::transact { @@ -81,7 +95,7 @@ where .. } => { if transaction.action == TransactionAction::Create { - return true; + return (true, call_count); } } pallet_ethereum::Call::transact { @@ -89,7 +103,7 @@ where .. } => { if transaction.action == TransactionAction::Create { - return true; + return (true, call_count); } } // Inconclusive, other calls might create contracts. @@ -100,11 +114,11 @@ where if let Some(pallet_evm::Call::create { .. } | pallet_evm::Call::create2 { .. }) = call.maybe_into_evm_call() { - return true; + return (true, call_count); } } - false + (false, call_count) } /// Reject contract creation, unless the account is in the current evm contract allow list. @@ -139,29 +153,53 @@ where as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner> + Clone, { - fn do_validate_unsigned(call: &RuntimeCallFor) -> TransactionValidity { - if !is_create_unsigned_contract_allowed::(call) { + pub(crate) fn do_validate_unsigned( + call: &RuntimeCallFor, + ) -> Result<(ValidTransaction, u32), TransactionValidityError> { + let (is_allowed, call_count) = is_create_unsigned_contract_allowed::(call); + if !is_allowed { Err(InvalidTransaction::Custom(ERR_CONTRACT_CREATION_NOT_ALLOWED).into()) } else { - Ok(ValidTransaction::default()) + Ok((ValidTransaction::default(), call_count)) } } - fn do_validate( + pub(crate) fn do_validate_signed( origin: &OriginFor, call: &RuntimeCallFor, - ) -> TransactionValidity { + ) -> Result<(ValidTransaction, u32), TransactionValidityError> { let Some(who) = origin.as_system_origin_signer() else { // Reject unsigned contract creation unless anyone is allowed to create them. return Self::do_validate_unsigned(call); }; + // Reject contract creation unless the account is in the allow list. - if !is_create_contract_allowed::(call, who) { + let (is_allowed, call_count) = is_create_contract_allowed::(call, who); + if !is_allowed { Err(InvalidTransaction::Custom(ERR_CONTRACT_CREATION_NOT_ALLOWED).into()) } else { - Ok(ValidTransaction::default()) + Ok((ValidTransaction::default(), call_count)) } } + + pub fn get_weights(n: u32) -> Weight { + SubstrateWeightInfo::::evm_contract_check_multiple(n) + .max(SubstrateWeightInfo::::evm_contract_check_nested(n)) + } +} + +/// Data passed from prepare to post_dispatch. +#[derive(RuntimeDebugNoBound)] +pub enum Pre { + /// Refund this exact amount of weight. + Refund(Weight), +} + +/// Data passed from validate to prepare. +#[derive(RuntimeDebugNoBound)] +pub enum Val { + /// Partially refund, based on the actual number of calls. + PartialRefund(u32), } // Unsigned calls can't create contracts. Only pallet-evm and pallet-ethereum can create contracts. @@ -182,17 +220,15 @@ where Result>: From>, as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner> + Clone, + for<'a> &'a mut PostDispatchInfoOf>: Into<&'a mut PostDispatchInfo>, { const IDENTIFIER: &'static str = "CheckContractCreation"; type Implicit = (); - type Val = (); - type Pre = (); + type Val = Val; + type Pre = Pre; - // TODO: calculate proper weight for this extension - // Currently only accounts for storage read fn weight(&self, _: &RuntimeCallFor) -> Weight { - // there will always be one storage read for this call - ::DbWeight::get().reads(1) + Self::get_weights(MAXIMUM_NUMBER_OF_CALLS) } fn validate( @@ -205,18 +241,56 @@ where _inherited_implication: &impl Encode, _source: TransactionSource, ) -> ValidateResult> { - let validity = Self::do_validate(&origin, call)?; - Ok((validity, (), origin)) + let (validity, val) = if origin.as_system_origin_signer().is_some() { + let (valid, call_count) = Self::do_validate_signed(&origin, call)?; + (valid, Val::PartialRefund(call_count)) + } else { + let (valid, call_count) = Self::do_validate_unsigned(call)?; + (valid, Val::PartialRefund(call_count)) + }; + + Ok((validity, val, origin)) } - impl_tx_ext_default!(RuntimeCallFor; prepare); + fn prepare( + self, + val: Self::Val, + _origin: &DispatchOriginOf>, + _call: &RuntimeCallFor, + _info: &DispatchInfoOf>, + _len: usize, + ) -> Result { + let pre_dispatch_weights = Self::get_weights(MAXIMUM_NUMBER_OF_CALLS); + match val { + // Refund any extra call weight + // TODO: use frame_system::Pallet::::reclaim_weight when we upgrade to 40.1.0 + // See + Val::PartialRefund(calls) => { + let actual_weights = Self::get_weights(calls); + Ok(Pre::Refund( + pre_dispatch_weights.saturating_sub(actual_weights), + )) + } + } + } + + fn post_dispatch_details( + pre: Self::Pre, + _info: &DispatchInfoOf>, + _post_info: &PostDispatchInfoOf>, + _len: usize, + _result: &DispatchResult, + ) -> Result { + let Pre::Refund(weight) = pre; + Ok(weight) + } fn bare_validate( call: &RuntimeCallFor, _info: &DispatchInfoOf>, _len: usize, ) -> TransactionValidity { - Self::do_validate_unsigned(call) + Self::do_validate_unsigned(call).map(|(validity, _call_count)| validity) } fn bare_validate_and_prepare( @@ -227,4 +301,30 @@ where Self::do_validate_unsigned(call)?; Ok(()) } + + // Weights for bare calls are calculated in the runtime, and excess weight refunded here. + fn bare_post_dispatch( + _info: &DispatchInfoOf>, + post_info: &mut PostDispatchInfoOf>, + _len: usize, + _result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + let pre_dispatch_weights = Self::get_weights(MAXIMUM_NUMBER_OF_CALLS); + // The number of Ethereum calls in a RuntimeCall is always 1, this is checked by + // is_self_contained() in the runtime. + let actual_weights = Self::get_weights(1); + + // TODO: use frame_system::Pallet::::reclaim_weight when we upgrade to 40.1.0 + let unspent = pre_dispatch_weights.saturating_sub(actual_weights); + + // If we overcharged the weight, refund the extra weight. + let post_info = Into::<&mut PostDispatchInfo>::into(post_info); + if let Some(actual_weight) = post_info.actual_weight + && actual_weight.ref_time() >= pre_dispatch_weights.ref_time() + { + post_info.refund(unspent); + } + + Ok(()) + } } diff --git a/domains/pallets/evm-tracker/src/lib.rs b/domains/pallets/evm-tracker/src/lib.rs index 866a4887f22..c99356591fa 100644 --- a/domains/pallets/evm-tracker/src/lib.rs +++ b/domains/pallets/evm-tracker/src/lib.rs @@ -20,16 +20,30 @@ #[cfg(not(feature = "std"))] extern crate alloc; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + pub mod check_nonce; pub mod create_contract; pub mod fees; pub mod traits; +pub mod weights; pub use check_nonce::CheckNonce; use domain_runtime_primitives::EthereumAccountId; pub use pallet::*; use sp_core::U256; use sp_domains::PermissionedActionAllowedBy; +use sp_weights::Weight; + +/// Maximum number of calls we benchmarked for. +const MAXIMUM_NUMBER_OF_CALLS: u32 = 5_000; + +/// Weight functions needed for pallet_evm_tracker. +pub trait WeightInfo { + fn evm_contract_check_multiple(c: u32) -> Weight; + fn evm_contract_check_nested(c: u32) -> Weight; +} #[frame_support::pallet] mod pallet { diff --git a/domains/pallets/evm-tracker/src/weights.rs b/domains/pallets/evm-tracker/src/weights.rs new file mode 100644 index 00000000000..0ad15b2fe00 --- /dev/null +++ b/domains/pallets/evm-tracker/src/weights.rs @@ -0,0 +1,67 @@ + +//! Autogenerated weights for `pallet_evm_tracker` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 46.2.0 +//! DATE: 2025-06-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `MacBook-Pro`, CPU: `M1 Max` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// ./target/release/subspace-node +// domain +// benchmark +// pallet +// --runtime=./target/release/wbuild/evm-domain-runtime/evm_domain_runtime.compact.compressed.wasm +// --genesis-builder=none +// --steps=50 +// --repeat=20 +// --pallet=pallet_evm_tracker +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./domains/pallets/evm-tracker/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +// Manually added import +use crate as pallet_evm_tracker; + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_evm_tracker`. +pub struct SubstrateWeightInfo(PhantomData); +impl pallet_evm_tracker::WeightInfo for SubstrateWeightInfo { + /// Storage: `EVMNoncetracker::ContractCreationAllowedBy` (r:1 w:0) + /// Proof: `EVMNoncetracker::ContractCreationAllowedBy` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `c` is `[0, 5000]`. + fn evm_contract_check_multiple(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `209` + // Estimated: `1694` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(3_468_459, 0) + .saturating_add(Weight::from_parts(0, 1694)) + // Standard Error: 67 + .saturating_add(Weight::from_parts(8_703, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1)) + } + /// Storage: `EVMNoncetracker::ContractCreationAllowedBy` (r:1 w:0) + /// Proof: `EVMNoncetracker::ContractCreationAllowedBy` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `c` is `[0, 1300]`. + fn evm_contract_check_nested(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `209` + // Estimated: `1694` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(3_485_601, 0) + .saturating_add(Weight::from_parts(0, 1694)) + // Standard Error: 626 + .saturating_add(Weight::from_parts(114_709, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1)) + } +} diff --git a/domains/runtime/evm/Cargo.toml b/domains/runtime/evm/Cargo.toml index 8acf1cc4e9e..876d6159bdb 100644 --- a/domains/runtime/evm/Cargo.toml +++ b/domains/runtime/evm/Cargo.toml @@ -143,6 +143,7 @@ runtime-benchmarks = [ "pallet-domain-id/runtime-benchmarks", "pallet-ethereum/runtime-benchmarks", "pallet-evm/runtime-benchmarks", + "pallet-evm-tracker/runtime-benchmarks", "pallet-messenger/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", diff --git a/domains/runtime/evm/src/lib.rs b/domains/runtime/evm/src/lib.rs index 520221f3302..9e47833b63f 100644 --- a/domains/runtime/evm/src/lib.rs +++ b/domains/runtime/evm/src/lib.rs @@ -203,7 +203,9 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { dispatch_info: &DispatchInfoOf, len: usize, ) -> Option { - if !is_create_contract_allowed::(self, &(*info).into()) { + let (is_allowed, _call_count) = + is_create_contract_allowed::(self, &(*info).into()); + if !is_allowed { return Some(Err(InvalidTransaction::Custom( ERR_CONTRACT_CREATION_NOT_ALLOWED, ) @@ -222,7 +224,9 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { dispatch_info: &DispatchInfoOf, len: usize, ) -> Option> { - if !is_create_contract_allowed::(self, &(*info).into()) { + let (is_allowed, _call_count) = + is_create_contract_allowed::(self, &(*info).into()); + if !is_allowed { return Some(Err(InvalidTransaction::Custom( ERR_CONTRACT_CREATION_NOT_ALLOWED, ) @@ -267,9 +271,38 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { ) -> Option>> { match self { call @ RuntimeCall::Ethereum(pallet_ethereum::Call::transact { .. }) => { - Some(call.dispatch(RuntimeOrigin::from( + let post_info = call.dispatch(RuntimeOrigin::from( pallet_ethereum::RawOrigin::EthereumTransaction(info), - ))) + )); + + // is_self_contained() checks for an Ethereum call, which is always a single call. + // This call has the same number of contract checks as an EVM call, and similar + // fields, so we can use the EVM benchmark weight here. + let create_contract_ext_weight = CheckContractCreation::::get_weights(1); + + // Add the weight of the contract creation extension check to the post info + Some( + post_info + .map(|mut post_info| { + post_info.actual_weight = Some( + post_info + .actual_weight + .unwrap_or_default() + .saturating_add(create_contract_ext_weight), + ); + post_info + }) + .map_err(|mut err_with_post_info| { + err_with_post_info.post_info.actual_weight = Some( + err_with_post_info + .post_info + .actual_weight + .unwrap_or_default() + .saturating_add(create_contract_ext_weight), + ); + err_with_post_info + }), + ) } _ => None, } @@ -976,6 +1009,7 @@ mod benches { [domain_pallet_executive, ExecutivePallet] [pallet_messenger, Messenger] [pallet_messenger_from_consensus_extension, MessengerFromConsensusExtensionBench::] + [pallet_evm_tracker, EVMNoncetracker] [pallet_messenger_between_domains_extension, MessengerBetweenDomainsExtensionBench::] [pallet_timestamp, Timestamp] [pallet_utility, Utility] diff --git a/domains/test/runtime/evm/src/lib.rs b/domains/test/runtime/evm/src/lib.rs index 14fbcb81e6e..710dab08790 100644 --- a/domains/test/runtime/evm/src/lib.rs +++ b/domains/test/runtime/evm/src/lib.rs @@ -48,7 +48,7 @@ use pallet_evm::{ Account as EVMAccount, EnsureAddressNever, EnsureAddressRoot, FeeCalculator, IdentityAddressMapping, Runner, }; -use pallet_evm_tracker::create_contract::is_create_contract_allowed; +use pallet_evm_tracker::create_contract::{CheckContractCreation, is_create_contract_allowed}; use pallet_evm_tracker::traits::{MaybeIntoEthCall, MaybeIntoEvmCall}; use pallet_transporter::EndpointHandler; use parity_scale_codec::{Decode, DecodeLimit, Encode, MaxEncodedLen}; @@ -253,7 +253,9 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { dispatch_info: &DispatchInfoOf, len: usize, ) -> Option { - if !is_create_contract_allowed::(self, &(*info).into()) { + let (is_allowed, _call_count) = + is_create_contract_allowed::(self, &(*info).into()); + if !is_allowed { return Some(Err(InvalidTransaction::Custom( ERR_CONTRACT_CREATION_NOT_ALLOWED, ) @@ -272,7 +274,9 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { dispatch_info: &DispatchInfoOf, len: usize, ) -> Option> { - if !is_create_contract_allowed::(self, &(*info).into()) { + let (is_allowed, _call_count) = + is_create_contract_allowed::(self, &(*info).into()); + if !is_allowed { return Some(Err(InvalidTransaction::Custom( ERR_CONTRACT_CREATION_NOT_ALLOWED, ) @@ -317,9 +321,38 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { ) -> Option>> { match self { call @ RuntimeCall::Ethereum(pallet_ethereum::Call::transact { .. }) => { - Some(call.dispatch(RuntimeOrigin::from( + let post_info = call.dispatch(RuntimeOrigin::from( pallet_ethereum::RawOrigin::EthereumTransaction(info), - ))) + )); + + // is_self_contained() checks for an Ethereum call, which is always a single call. + // This call has the same number of contract checks as an EVM call, and similar + // fields, so we can use the EVM benchmark weight here. + let create_contract_ext_weight = CheckContractCreation::::get_weights(1); + + // Add the weight of the contract creation extension check to the post info + Some( + post_info + .map(|mut post_info| { + post_info.actual_weight = Some( + post_info + .actual_weight + .unwrap_or_default() + .saturating_add(create_contract_ext_weight), + ); + post_info + }) + .map_err(|mut err_with_post_info| { + err_with_post_info.post_info.actual_weight = Some( + err_with_post_info + .post_info + .actual_weight + .unwrap_or_default() + .saturating_add(create_contract_ext_weight), + ); + err_with_post_info + }), + ) } _ => None, }