Skip to content

Commit

Permalink
Restore accounts with missing state (#1402)
Browse files Browse the repository at this point in the history
  • Loading branch information
quasiyoke committed Feb 10, 2025
1 parent 69f1928 commit dad2745
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 77 deletions.
20 changes: 17 additions & 3 deletions crates/humanode-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,10 +880,24 @@ 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);
fn has_code(account_id: &EvmAccountId) -> (bool, Weight) {
let has_code = pallet_evm::AccountCodes::<Runtime>::contains_key(account_id);
let weight = <Runtime as frame_system::Config>::DbWeight::get().reads(1);
(flag, weight)
(has_code, weight)
}

fn accounts_managed_by_evm() -> impl Iterator<Item = (EvmAccountId, Weight)> {
let precompiles: sp_std::collections::btree_set::BTreeSet<_> =
FrontierPrecompiles::<Runtime>::used_addresses()
.into_iter()
.collect();
pallet_evm::AccountCodes::<Runtime>::iter_keys().filter_map(move |account_id| {
let is_precompiled = precompiles.contains(&account_id);
(!is_precompiled).then(|| {
let weight = <Runtime as frame_system::Config>::DbWeight::get().reads(1);
(account_id, weight)
})
})
}
}

Expand Down
133 changes: 59 additions & 74 deletions crates/pallet-evm-system/src/migrations/broken_nonces_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ 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);
/// Check whether account address has code.
fn has_code(account_id: &AccountId) -> (bool, Weight);

/// Gives an iterator over accounts managed by EVM. Considers precompiled contracts' accounts
/// as not "managed by EVM".
fn accounts_managed_by_evm() -> impl Iterator<Item = (AccountId, Weight)>;
}

/// Execute migration to recover broken nonces.
Expand All @@ -37,16 +41,19 @@ where
let pallet_name = Pallet::<T>::name();
info!("{pallet_name}: Running migration to recover broken nonces");

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

<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)
},
);
let accounts_count = EP::accounts_managed_by_evm()
.map(|(id, retrieval_weight)| {
weight.saturating_accrue(retrieval_weight);
let recovery_weight =
<Account<T>>::mutate_exists(id, |account| Self::recover(&id, account));
weight.saturating_accrue(recovery_weight);
})
.count()
.try_into()
.expect("Accounts count mustn't overflow");
weight.saturating_accrue(T::DbWeight::get().reads_writes(accounts_count, accounts_count));

info!("{pallet_name}: Migrated");

Expand All @@ -58,7 +65,7 @@ where
let accounts = <Account<T>>::iter_keys()
.count()
.try_into()
.expect("Accounts count must not overflow");
.expect("Accounts count mustn't overflow");
Ok(PreUpgradeState { accounts }.encode())
}

Expand All @@ -67,20 +74,21 @@ where
let accounts_count: u64 = <Account<T>>::iter_keys()
.count()
.try_into()
.expect("Accounts count must not overflow");
.expect("Accounts count mustn't overflow");
let PreUpgradeState {
accounts: expected_accounts_count,
accounts: prev_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",
accounts_count >= prev_accounts_count,
"Accounts count shouldn't decrease",
);

let accounts_to_recover = <Account<T>>::iter()
.filter(|(account_id, account)| {
let (is_broken, _weight) = Self::has_broken_nonce(account_id, account);
is_broken
let accounts_to_recover = EP::accounts_managed_by_evm()
.filter(|(account_id, _weight)| {
<Account<T>>::try_get(account_id)
.map(|account| account.nonce.is_zero())
.unwrap_or(true)
})
.count();
ensure!(
Expand All @@ -97,41 +105,35 @@ where
T: Config<AccountId = H160>,
<T as Config>::Index: rlp::Encodable,
{
/// Checks account state and recovers it if necessary.
/// Checks the contract account state and recovers it if necessary.
/// - If the state is missing, recreates it.
/// - If the state has nonce = 0, writes the smallest possible valid nonce.
///
/// Precompiled contracts in Ethereum usually have nonce = 0 as well. However, 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`. Therefore,
/// precompiled contracts MUST NOT be passed here.
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,
id: &<T as Config>::AccountId,
account: &mut Option<AccountInfo<<T as Config>::Index, <T as Config>::AccountData>>,
) -> Weight {
let Some(account) = account.as_mut() else {
info!("Account {id} lacks its state");
let (nonce, weight) = Self::min_nonce(id);
*account = Some(AccountInfo {
nonce,
data: Default::default(),
});
return weight;
};
(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());
if !account.nonce.is_zero() {
return Default::default();
}
EP::is_managed_by_evm(account_id)
info!("Account {id} has zero nonce");
let (nonce, weight) = Self::min_nonce(id);
account.nonce = nonce;
weight
}

/// Computes the minimum possible nonce for a given account.
Expand All @@ -140,28 +142,21 @@ where
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);
let (has_code, w) = EP::has_code(&contract_id);
weight.saturating_accrue(w);
is_occupied
let has_state = <Account<T>>::contains_key(contract_id);
weight.saturating_accrue(T::DbWeight::get().reads(1));
has_code || has_state
} {
nonce = nonce
.checked_add(&One::one())
.expect("Nonce mustn't overflow");
.expect("Nonce value 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
Expand All @@ -180,16 +175,6 @@ mod test {

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(
Expand Down

0 comments on commit dad2745

Please sign in to comment.