Skip to content

Commit

Permalink
Migrate EVM-managed accounts' state with broken nonce;
Browse files Browse the repository at this point in the history
compute min unused nonce (#1402)
  • Loading branch information
quasiyoke committed Feb 10, 2025
1 parent 331a20e commit 69f1928
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
19 changes: 19 additions & 0 deletions crates/humanode-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,13 +874,32 @@ pub type UncheckedExtrinsic =
/// The payload being signed in transactions.
pub type SignedPayload = generic::SignedPayload<RuntimeCall, SignedExtra>;

/// EVM state provider.
pub struct EvmStateProvider;

impl pallet_evm_system::migrations::broken_nonces_recovery::EvmStateProvider<EvmAccountId>
for EvmStateProvider
{
fn is_managed_by_evm(account_id: &EvmAccountId) -> (bool, Weight) {
let flag = pallet_evm::AccountCodes::<Runtime>::contains_key(account_id);
let weight = <Runtime as frame_system::Config>::DbWeight::get().reads(1);
(flag, weight)
}
}

/// Executive: handles dispatch to the various modules.
pub type Executive = frame_executive::Executive<
Runtime,
Block,
frame_system::ChainContext<Runtime>,
Runtime,
AllPalletsWithSystem,
(
pallet_evm_system::migrations::broken_nonces_recovery::MigrationBrokenNoncesRecovery<
EvmStateProvider,
Runtime,
>,
),
>;

impl frame_system::offchain::CreateSignedTransaction<RuntimeCall> for Runtime {
Expand Down
7 changes: 5 additions & 2 deletions crates/pallet-evm-system/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/pallet-evm-system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use sp_runtime::{
DispatchError, RuntimeDebug, Saturating,
};

pub mod migrations;
#[cfg(test)]
mod mock;
#[cfg(test)]
Expand Down
204 changes: 204 additions & 0 deletions crates/pallet-evm-system/src/migrations/broken_nonces_recovery.rs
Original file line number Diff line number Diff line change
@@ -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<AccountId> {
/// 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<EP, T>(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<EP, T> OnRuntimeUpgrade for MigrationBrokenNoncesRecovery<EP, T>
where
EP: EvmStateProvider<<T as Config>::AccountId>,
T: Config<AccountId = H160>,
<T as Config>::Index: rlp::Encodable,
{
fn on_runtime_upgrade() -> Weight {
let pallet_name = Pallet::<T>::name();
info!("{pallet_name}: Running migration to recover broken nonces");

let mut weight: Weight = T::DbWeight::get().reads(1);

<Account<T>>::translate::<AccountInfo<<T as Config>::Index, <T as Config>::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<Vec<u8>, &'static str> {
let accounts = <Account<T>>::iter_keys()
.count()
.try_into()
.expect("Accounts count must not overflow");
Ok(PreUpgradeState { accounts }.encode())
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), &'static str> {
let accounts_count: u64 = <Account<T>>::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 = <Account<T>>::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<EP, T> MigrationBrokenNoncesRecovery<EP, T>
where
EP: EvmStateProvider<<T as Config>::AccountId>,
T: Config<AccountId = H160>,
<T as Config>::Index: rlp::Encodable,
{
/// Checks account state and recovers it if necessary.
fn recover(
account_id: &<T as Config>::AccountId,
account: AccountInfo<<T as Config>::Index, <T as Config>::AccountData>,
) -> (
AccountInfo<<T as Config>::Index, <T as Config>::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: &<T as Config>::AccountId,
account: &AccountInfo<<T as Config>::Index, <T as Config>::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: &<T as Config>::AccountId) -> (<T as Config>::Index, Weight) {
let mut weight = Weight::default();
let mut nonce = <T as Config>::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(),
);
}
}
3 changes: 3 additions & 0 deletions crates/pallet-evm-system/src/migrations/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! State migrations.
pub mod broken_nonces_recovery;

0 comments on commit 69f1928

Please sign in to comment.