diff --git a/Cargo.lock b/Cargo.lock index 4c0e7f4083..6b3c7c068e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5442,6 +5442,23 @@ dependencies = [ "sp-io", ] +[[package]] +name = "pallet-evm-system" +version = "1.0.0-dev" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "log", + "mockall", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-test-vector-support" version = "1.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 2c8c6ffbe7..77a3844afb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "frame/ethereum", "frame/evm", "frame/evm-chain-id", + "frame/evm-system", "frame/hotfix-sufficients", "frame/evm/precompile/sha3fips", "frame/evm/precompile/simple", @@ -51,6 +52,7 @@ kvdb-rocksdb = "0.17.0" libsecp256k1 = { version = "0.7.1", default-features = false } log = { version = "0.4.17", default-features = false } parity-db = "0.4.6" +mockall = "0.11" rlp = { version = "0.5", default-features = false } scale-codec = { package = "parity-scale-codec", version = "3.2.1", default-features = false, features = ["derive"] } scale-info = { version = "2.3.1", default-features = false, features = ["derive"] } @@ -149,6 +151,7 @@ pallet-dynamic-fee = { version = "4.0.0-dev", path = "frame/dynamic-fee", defaul pallet-ethereum = { version = "4.0.0-dev", path = "frame/ethereum", default-features = false } pallet-evm = { version = "6.0.0-dev", path = "frame/evm", default-features = false } pallet-evm-chain-id = { version = "1.0.0-dev", path = "frame/evm-chain-id", default-features = false } +pallet-evm-system = { version = "1.0.0-dev", path = "frame/evm-system", default-features = false } pallet-evm-precompile-modexp = { version = "2.0.0-dev", path = "frame/evm/precompile/modexp", default-features = false } pallet-evm-precompile-sha3fips = { version = "2.0.0-dev", path = "frame/evm/precompile/sha3fips", default-features = false } pallet-evm-precompile-simple = { version = "2.0.0-dev", path = "frame/evm/precompile/simple", default-features = false } diff --git a/frame/evm-system/Cargo.toml b/frame/evm-system/Cargo.toml new file mode 100644 index 0000000000..1a114304fe --- /dev/null +++ b/frame/evm-system/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-evm-system" +version = "1.0.0-dev" +license = "Apache-2.0" +description = "FRAME EVM SYSTEM pallet." +edition = { workspace = true } +repository = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +log = { workspace = true, default-features = false } +scale-codec = { package = "parity-scale-codec", workspace = true } +scale-info = { workspace = true } +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +# Frontier +fp-evm = { workspace = true } + +[dev-dependencies] +mockall = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "log/std", + "scale-codec/std", + "scale-info/std", + # Substrate + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-std/std", + # Frontier + "fp-evm/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/frame/evm-system/src/lib.rs b/frame/evm-system/src/lib.rs new file mode 100644 index 0000000000..66ff84fc79 --- /dev/null +++ b/frame/evm-system/src/lib.rs @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! # EVM System Pallet. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::StoredMap; +use sp_runtime::{traits::One, RuntimeDebug, DispatchResult, DispatchError}; +use scale_codec::{Encode, Decode, MaxEncodedLen, FullCodec}; +use scale_info::TypeInfo; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use pallet::*; + +/// Account information. +#[derive(Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub struct AccountInfo { + /// The number of transactions this account has sent. + pub nonce: Index, + /// The additional data that belongs to this account. Used to store the balance(s) in a lot of + /// chains. + pub data: AccountData, +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use sp_runtime::traits::{MaybeDisplay, AtLeast32Bit}; + use sp_std::fmt::Debug; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The user account identifier type. + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + /// Account index (aka nonce) type. This stores the number of previous transactions + /// associated with a sender account. + type Index: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + Default + + MaybeDisplay + + AtLeast32Bit + + Copy + + MaxEncodedLen; + + /// Data to be associated with an account (other than nonce/transaction counter, which this + /// pallet does regardless). + type AccountData: Member + FullCodec + Clone + Default + TypeInfo + MaxEncodedLen; + + /// Handler for when a new account has just been created. + type OnNewAccount: OnNewAccount<::AccountId>; + + /// A function that is invoked when an account has been determined to be dead. + /// + /// All resources should be cleaned up associated with the given account. + type OnKilledAccount: OnKilledAccount<::AccountId>; + } + + /// The full account information for a particular account ID. + #[pallet::storage] + pub type Account = StorageMap< + _, + Blake2_128Concat, + ::AccountId, + AccountInfo<::Index, ::AccountData>, + ValueQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new account was created. + NewAccount { account: ::AccountId }, + /// An account was reaped. + KilledAccount { account: ::AccountId }, + } + + #[pallet::error] + pub enum Error { + /// The account already exists in case creating it. + AccountAlreadyExist, + /// The account doesn't exist in case removing it. + AccountNotExist, + } +} + +impl Pallet { + /// Check the account existence. + pub fn account_exists(who: &::AccountId) -> bool { + Account::::contains_key(who) + } + + /// An account is being created. + fn on_created_account(who: ::AccountId) { + ::OnNewAccount::on_new_account(&who); + Self::deposit_event(Event::NewAccount { account: who }); + } + + /// Do anything that needs to be done after an account has been killed. + fn on_killed_account(who: ::AccountId) { + ::OnKilledAccount::on_killed_account(&who); + Self::deposit_event(Event::KilledAccount { account: who }); + } + + /// Retrieve the account transaction counter from storage. + pub fn account_nonce(who: &::AccountId) -> ::Index { + Account::::get(who).nonce + } + + /// Increment a particular account's nonce by 1. + pub fn inc_account_nonce(who: &::AccountId) { + Account::::mutate(who, |a| a.nonce += ::Index::one()); + } + + /// Create an account. + pub fn create_account(who: &::AccountId) -> DispatchResult { + if Self::account_exists(who) { + return Err(Error::::AccountAlreadyExist.into()); + } + + Account::::insert(who.clone(), AccountInfo::<_, _>::default()); + Self::on_created_account(who.clone()); + Ok(()) + } + + /// Remove an account. + pub fn remove_account(who: &::AccountId) -> DispatchResult { + if !Self::account_exists(who) { + return Err(Error::::AccountNotExist.into()); + } + + Account::::remove(who); + Self::on_killed_account(who.clone()); + Ok(()) + } +} + +impl StoredMap<::AccountId, ::AccountData> for Pallet { + fn get(k: &::AccountId) -> ::AccountData { + Account::::get(k).data + } + + fn try_mutate_exists>( + k: &::AccountId, + f: impl FnOnce(&mut Option<::AccountData>) -> Result, + ) -> Result { + let (mut maybe_account_data, was_providing) = if Self::account_exists(k) { + (Some(Account::::get(k).data), true) + } else { + (None, false) + }; + + let result = f(&mut maybe_account_data)?; + + match (maybe_account_data, was_providing) { + (Some(data), false) => { + Account::::mutate(k, |a| a.data = data); + Self::on_created_account(k.clone()); + } + (Some(data), true) => { + Account::::mutate(k, |a| a.data = data); + } + (None, true) => { + Account::::remove(k); + Self::on_killed_account(k.clone()); + } + (None, false) => { + // Do nothing. + } + } + + Ok(result) + } +} + +impl fp_evm::AccountProvider for Pallet { + type AccountId = ::AccountId; + type Index = ::Index; + + fn create_account(who: &Self::AccountId) { + let _ = Self::create_account(who); + } + + fn remove_account(who: &Self::AccountId) { + let _ = Self::remove_account(who); + } + + fn account_nonce(who: &Self::AccountId) -> Self::Index { + Self::account_nonce(who) + } + + fn inc_account_nonce(who: &Self::AccountId) { + Self::inc_account_nonce(who); + } +} + +/// Interface to handle account creation. +pub trait OnNewAccount { + /// A new account `who` has been registered. + fn on_new_account(who: &AccountId); +} + +impl OnNewAccount for () { + fn on_new_account(_who: &AccountId) {} +} + +/// Interface to handle account killing. +pub trait OnKilledAccount { + /// The account with the given id was reaped. + fn on_killed_account(who: &AccountId); +} + +impl OnKilledAccount for () { + fn on_killed_account(_who: &AccountId) {} +} diff --git a/frame/evm-system/src/mock.rs b/frame/evm-system/src/mock.rs new file mode 100644 index 0000000000..0782c3c758 --- /dev/null +++ b/frame/evm-system/src/mock.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// This file is part of Frontier. +// +// Copyright (c) 2020-2022 Parity Technologies (UK) Ltd. +// +// 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. + +//! Test mock for unit tests. + +use frame_support::{ + traits::{ConstU32, ConstU64}, +}; +use mockall::mock; +use sp_core::{H160, H256}; +use sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentityLookup}, BuildStorage, +}; +use sp_std::{boxed::Box, prelude::*}; + +use crate::{self as pallet_evm_system, *}; + +mock! { + #[derive(Debug)] + pub DummyOnNewAccount {} + + impl OnNewAccount for DummyOnNewAccount { + pub fn on_new_account(who: &H160); + } +} + +mock! { + #[derive(Debug)] + pub DummyOnKilledAccount {} + + impl OnKilledAccount for DummyOnKilledAccount { + pub fn on_killed_account(who: &H160); + } +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime! { + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + EvmSystem: pallet_evm_system, + } +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = H160; + type Lookup = IdentityLookup; + type Header = generic::Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_evm_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = H160; + type Index = u64; + type AccountData = u64; + type OnNewAccount = MockDummyOnNewAccount; + type OnKilledAccount = MockDummyOnKilledAccount; +} + +/// Build test externalities from the custom genesis. +/// Using this call requires manual assertions on the genesis init logic. +pub fn new_test_ext() -> sp_io::TestExternalities { + // Build genesis. + let config = GenesisConfig { + ..Default::default() + }; + let storage = config.build_storage().unwrap(); + + // Make test externalities from the storage. + storage.into() +} + +pub fn runtime_lock() -> std::sync::MutexGuard<'static, ()> { + static MOCK_RUNTIME_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + // Ignore the poisoning for the tests that panic. + // We only care about concurrency here, not about the poisoning. + match MOCK_RUNTIME_MUTEX.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +pub trait TestExternalitiesExt { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R; +} + +impl TestExternalitiesExt for frame_support::sp_io::TestExternalities { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R, + { + let guard = runtime_lock(); + let result = self.execute_with(|| execute(&guard)); + drop(guard); + result + } +} + diff --git a/frame/evm-system/src/tests.rs b/frame/evm-system/src/tests.rs new file mode 100644 index 0000000000..28fc971842 --- /dev/null +++ b/frame/evm-system/src/tests.rs @@ -0,0 +1,291 @@ +//! Unit tests. + +use sp_std::str::FromStr; + +use frame_support::{assert_ok, assert_noop}; +use mockall::predicate; +use sp_core::H160; + +use crate::{mock::*, *}; + +/// This test verifies that creating account works in the happy path. +#[test] +fn create_account_works() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + assert!(!EvmSystem::account_exists(&account_id)); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + let on_new_account_ctx = MockDummyOnNewAccount::on_new_account_context(); + on_new_account_ctx + .expect() + .once() + .with( + predicate::eq(account_id), + ) + .return_const(()); + + // Invoke the function under test. + assert_ok!(EvmSystem::create_account(&account_id)); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::NewAccount { account: account_id } )); + + // Assert mock invocations. + on_new_account_ctx.checkpoint(); + }); +} + +/// This test verifies that creating account fails when the account already exists. +#[test] +fn create_account_fails() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + >::insert(account_id.clone(), AccountInfo::<_, _>::default()); + + // Invoke the function under test. + assert_noop!(EvmSystem::create_account(&account_id), Error::::AccountAlreadyExist); + }); +} + +/// This test verifies that removing account works in the happy path. +#[test] +fn remove_account_works() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + >::insert(account_id.clone(), AccountInfo::<_, _>::default()); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + let on_killed_account_ctx = MockDummyOnKilledAccount::on_killed_account_context(); + on_killed_account_ctx + .expect() + .once() + .with( + predicate::eq(account_id), + ) + .return_const(()); + + // Invoke the function under test. + assert_ok!(EvmSystem::remove_account(&account_id)); + + // Assert state changes. + assert!(!EvmSystem::account_exists(&account_id)); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::KilledAccount { account: account_id } )); + + // Assert mock invocations. + on_killed_account_ctx.checkpoint(); + }); +} + +/// This test verifies that removing account fails when the account doesn't exist. +#[test] +fn remove_account_fails() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Invoke the function under test. + assert_noop!(EvmSystem::remove_account(&account_id), Error::::AccountNotExist); + }); +} + +/// This test verifies that incrementing account nonce works in the happy path. +#[test] +fn inc_account_nonce_works() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + let nonce_before = EvmSystem::account_nonce(&account_id); + + // Invoke the function under test. + EvmSystem::inc_account_nonce(&account_id); + + // Assert state changes. + assert_eq!(EvmSystem::account_nonce(&account_id), nonce_before + 1); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data wasn't providing +/// and returned data is `Some`. As a result, a new account has been created. +#[test] +fn try_mutate_exists_account_created() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + assert!(!EvmSystem::account_exists(&account_id)); + + // Set mock expectations. + let on_new_account_ctx = MockDummyOnNewAccount::on_new_account_context(); + on_new_account_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(()); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&account_id, |maybe_data| -> Result<(), DispatchError> { + *maybe_data = Some(1); + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + assert_eq!(EvmSystem::get(&account_id), 1); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::NewAccount { + account: account_id, + })); + + // Assert mock invocations. + on_new_account_ctx.checkpoint(); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data was providing +/// and returned data is `Some`. As a result, the account has been updated. +#[test] +fn try_mutate_exists_account_updated() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 1; + let data = 1; + >::insert(account_id.clone(), AccountInfo { nonce, data }); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&account_id)); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&account_id, |maybe_data| -> Result<(), DispatchError> { + if let Some(ref mut data) = maybe_data { + *data += 1; + } + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + assert_eq!(EvmSystem::get(&account_id), data + 1); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data was providing +/// and returned data is `None`. As a result, the account has been removed. +#[test] +fn try_mutate_exists_account_removed() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 1; + let data = 1; + >::insert(account_id.clone(), AccountInfo { nonce, data }); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&account_id)); + + // Set mock expectations. + let on_killed_account_ctx = MockDummyOnKilledAccount::on_killed_account_context(); + on_killed_account_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(()); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&account_id, |maybe_data| -> Result<(), DispatchError> { + *maybe_data = None; + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(!EvmSystem::account_exists(&account_id)); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::KilledAccount { + account: account_id, + })); + + // Assert mock invocations. + on_killed_account_ctx.checkpoint(); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data wasn't providing +/// and returned data is `None`. As a result, the account hasn't been created. +#[test] +fn try_mutate_exists_account_not_created() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + assert!(!EvmSystem::account_exists(&account_id)); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + >::try_mutate_exists(account_id, |maybe_data| -> Result<(), ()> { + *maybe_data = None; + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(!EvmSystem::account_exists(&account_id)); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case getting error +/// during data mutation. +#[test] +fn try_mutate_exists_fails_without_changes() { + new_test_ext().execute_with(|| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 1; + let data = 1; + >::insert(account_id.clone(), AccountInfo { nonce, data }); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&account_id)); + + // Invoke the function under test. + assert_noop!( + >::try_mutate_exists(account_id, |maybe_data| -> Result<(), ()> { + *maybe_data = None; + Err(()) + }), + () + ); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + assert_eq!(EvmSystem::get(&account_id), data); + }); +}