diff --git a/Cargo.lock b/Cargo.lock index f800dc79a..7b9ef1f4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6634,8 +6634,10 @@ dependencies = [ "fp-evm", "frame-support", "frame-system", + "hex-literal", "mockall", "parity-scale-codec", + "rlp", "scale-info", "sp-core", "sp-io", diff --git a/Cargo.toml b/Cargo.toml index 148e04d77..f443b1725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ qr2term = { version = "0.3", default-features = false } quote = { version = "1.0", default-features = false } rand = { version = "0.8", default-features = false } reqwest = { version = "0.11", default-features = false } +rlp = { version = "0.5", default-features = false } rustc-hex = { version = "2", default-features = false } scale-info = { version = "2.11.6", default-features = false } secp256k1 = { version = "0.27", default-features = false } diff --git a/crates/humanode-runtime/src/lib.rs b/crates/humanode-runtime/src/lib.rs index 98449af73..af7eb8894 100644 --- a/crates/humanode-runtime/src/lib.rs +++ b/crates/humanode-runtime/src/lib.rs @@ -874,6 +874,19 @@ pub type UncheckedExtrinsic = /// The payload being signed in transactions. pub type SignedPayload = generic::SignedPayload; +/// EVM state provider. +pub struct EvmStateProvider; + +impl pallet_evm_system::migrations::broken_nonces_recovery::EvmStateProvider + for EvmStateProvider +{ + fn is_managed_by_evm(account_id: &EvmAccountId) -> (bool, Weight) { + let flag = pallet_evm::AccountCodes::::contains_key(account_id); + let weight = ::DbWeight::get().reads(1); + (flag, weight) + } +} + /// Executive: handles dispatch to the various modules. pub type Executive = frame_executive::Executive< Runtime, @@ -881,6 +894,12 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, + ( + pallet_evm_system::migrations::broken_nonces_recovery::MigrationBrokenNoncesRecovery< + EvmStateProvider, + Runtime, + >, + ), >; impl frame_system::offchain::CreateSignedTransaction for Runtime { diff --git a/crates/pallet-evm-system/Cargo.toml b/crates/pallet-evm-system/Cargo.toml index 2e00f444f..740856d4b 100644 --- a/crates/pallet-evm-system/Cargo.toml +++ b/crates/pallet-evm-system/Cargo.toml @@ -9,14 +9,16 @@ codec = { workspace = true } fp-evm = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } +rlp = { workspace = true } scale-info = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } [dev-dependencies] +hex-literal = { workspace = true } mockall = { workspace = true } -sp-core = { workspace = true } -sp-io = { workspace = true } [features] default = ["std"] @@ -25,6 +27,7 @@ std = [ "fp-evm/std", "frame-support/std", "frame-system/std", + "rlp/std", "scale-info/std", "sp-core/std", "sp-io/std", diff --git a/crates/pallet-evm-system/src/lib.rs b/crates/pallet-evm-system/src/lib.rs index 172e651c5..37b03266b 100644 --- a/crates/pallet-evm-system/src/lib.rs +++ b/crates/pallet-evm-system/src/lib.rs @@ -10,6 +10,7 @@ use sp_runtime::{ DispatchError, RuntimeDebug, Saturating, }; +pub mod migrations; #[cfg(test)] mod mock; #[cfg(test)] diff --git a/crates/pallet-evm-system/src/migrations/broken_nonces_recovery.rs b/crates/pallet-evm-system/src/migrations/broken_nonces_recovery.rs new file mode 100644 index 000000000..ee39c7eb9 --- /dev/null +++ b/crates/pallet-evm-system/src/migrations/broken_nonces_recovery.rs @@ -0,0 +1,204 @@ +//! Migration to recover broken nonces. + +#[cfg(feature = "try-runtime")] +use frame_support::sp_std::vec::Vec; +use frame_support::{log::info, pallet_prelude::*, traits::OnRuntimeUpgrade}; +use rlp::RlpStream; +use sp_core::H160; +use sp_io::hashing::keccak_256; +use sp_runtime::traits::{CheckedAdd, One, Zero}; + +use crate::{Account, AccountInfo, Config, Pallet}; + +/// EVM state provider. +pub trait EvmStateProvider { + /// Check whether account is managed by EVM or not. + fn is_managed_by_evm(account_id: &AccountId) -> (bool, Weight); +} + +/// Execute migration to recover broken nonces. +pub struct MigrationBrokenNoncesRecovery(sp_std::marker::PhantomData<(EP, T)>); + +#[cfg(feature = "try-runtime")] +#[derive(Encode, Decode)] +/// Key indicators of the state before runtime upgrade. +struct PreUpgradeState { + /// Accounts' count. + accounts: u64, +} + +impl OnRuntimeUpgrade for MigrationBrokenNoncesRecovery +where + EP: EvmStateProvider<::AccountId>, + T: Config, + ::Index: rlp::Encodable, +{ + fn on_runtime_upgrade() -> Weight { + let pallet_name = Pallet::::name(); + info!("{pallet_name}: Running migration to recover broken nonces"); + + let mut weight: Weight = T::DbWeight::get().reads(1); + + >::translate::::Index, ::AccountData>, _>( + |id, account| { + let (account, w) = Self::recover(&id, account); + weight.saturating_accrue(w); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + Some(account) + }, + ); + + info!("{pallet_name}: Migrated"); + + weight + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + let accounts = >::iter_keys() + .count() + .try_into() + .expect("Accounts count must not overflow"); + Ok(PreUpgradeState { accounts }.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), &'static str> { + let accounts_count: u64 = >::iter_keys() + .count() + .try_into() + .expect("Accounts count must not overflow"); + let PreUpgradeState { + accounts: expected_accounts_count, + } = Decode::decode(&mut state.as_slice()) + .map_err(|_err| "Failed pre-upgrade state decoding")?; + ensure!( + accounts_count == expected_accounts_count, + "Accounts count shouldn't change", + ); + + let accounts_to_recover = >::iter() + .filter(|(account_id, account)| { + let (is_broken, _weight) = Self::has_broken_nonce(account_id, account); + is_broken + }) + .count(); + ensure!( + accounts_to_recover == 0, + "There should be no accounts left for recovery", + ); + Ok(()) + } +} + +impl MigrationBrokenNoncesRecovery +where + EP: EvmStateProvider<::AccountId>, + T: Config, + ::Index: rlp::Encodable, +{ + /// Checks account state and recovers it if necessary. + fn recover( + account_id: &::AccountId, + account: AccountInfo<::Index, ::AccountData>, + ) -> ( + AccountInfo<::Index, ::AccountData>, + Weight, + ) { + let (is_broken, mut weight) = Self::has_broken_nonce(account_id, &account); + if !is_broken { + return (account, weight); + } + info!("Account {account_id} requires recovery"); + let (nonce, nonce_weight) = Self::min_nonce(account_id); + weight.saturating_accrue(nonce_weight); + let account = AccountInfo { + nonce, + data: account.data, + }; + (account, weight) + } + + /// Checks if an account's nonce needs to be recovered. + fn has_broken_nonce( + account_id: &::AccountId, + account: &AccountInfo<::Index, ::AccountData>, + ) -> (bool, Weight) { + if !account.nonce.is_zero() || is_precompiled(account_id) { + // Precompiled contracts in Ethereum usually have nonce = 0. Since precompiled contracts are typically + // implemented by hooking calls to specific addresses and adding dummy state (to ensure they are callable + // like regular contracts), there's no need for a non-zero nonce unless they explicitly perform + // state-changing operations like `CREATE`. + return (false, Default::default()); + } + EP::is_managed_by_evm(account_id) + } + + /// Computes the minimum possible nonce for a given account. + fn min_nonce(id: &::AccountId) -> (::Index, Weight) { + let mut weight = Weight::default(); + let mut nonce = ::Index::one(); + while { + let contract_id = contract_address(id, nonce); + let (is_occupied, w) = EP::is_managed_by_evm(&contract_id); + weight.saturating_accrue(w); + is_occupied + } { + nonce = nonce + .checked_add(&One::one()) + .expect("Nonce mustn't overflow"); + } + info!("Account {id} minimal valid nonce is {nonce:?}"); + (nonce, weight) + } +} + +/// Checks if the given account is precompiled. +fn is_precompiled(address: &H160) -> bool { + /// The largest precompiled address we currently have by numeric value is 0x900. + const ZERO_PREFIX_LENGTH: usize = (160 - 16) / 8; + address.as_bytes()[..ZERO_PREFIX_LENGTH] + .iter() + .all(Zero::is_zero) +} + +/// Contract address that will be produced by the [`CREATE` opcode][1]. +/// +/// [1]: https://ethereum.github.io/yellowpaper/paper.pdf#section.7 +fn contract_address(sender: &H160, nonce: impl rlp::Encodable) -> H160 { + let mut rlp = RlpStream::new_list(2); + rlp.append(sender); + rlp.append(&nonce); + /// Address is the rightmost 160 bits of hash. + const ADDR_OFFSET: usize = (256 - 160) / 8; + H160::from_slice(&keccak_256(&rlp.out())[ADDR_OFFSET..]) +} + +#[cfg(test)] +mod test { + use hex_literal::hex; + + use super::*; + + #[test] + fn is_precompiled_detects_precompiled_contracts() { + assert!(is_precompiled( + &hex!("0000000000000000000000000000000000000900").into(), + )); + assert!(!is_precompiled( + &hex!("f803e8ca755ae4770b5e6072a1e3cb97631d76ee").into(), + )); + } + + #[test] + fn contract_address_produces_addresses() { + let addr = contract_address( + &hex!("f803e8ca755ae4770b5e6072a1e3cb97631d76ee").into(), + 1u32, + ); + assert_eq!( + addr, + hex!("efdd09582498184d14af330e1b02d0c8d63afed5").into(), + ); + } +} diff --git a/crates/pallet-evm-system/src/migrations/mod.rs b/crates/pallet-evm-system/src/migrations/mod.rs new file mode 100644 index 000000000..d7ee49b70 --- /dev/null +++ b/crates/pallet-evm-system/src/migrations/mod.rs @@ -0,0 +1,3 @@ +//! State migrations. + +pub mod broken_nonces_recovery;