diff --git a/CHANGELOG.md b/CHANGELOG.md index c0e3ac1198..c463416b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Chain-spec generator: propagate the `on_chain_release_build` feature to the chain-spec generator. Without this the live/genesis chain-specs contain a wrongly-configured WASM blob ([polkadot-fellows/runtimes#450](https://github.com/polkadot-fellows/runtimes/pull/450)). - Adds a migration to the Polkadot Coretime chain to fix an issue from the initial Coretime migration. ([polkadot-fellows/runtimes#458](https://github.com/polkadot-fellows/runtimes/pull/458)) +- Adds migrations to restore currupted staking ledgers in Polkadot and Kusama ([polkadot-fellows/runtimes#447](https://github.com/polkadot-fellows/runtimes/pull/447)) ### Added diff --git a/relay/kusama/src/lib.rs b/relay/kusama/src/lib.rs index 37a187396a..f0a4af8418 100644 --- a/relay/kusama/src/lib.rs +++ b/relay/kusama/src/lib.rs @@ -1817,12 +1817,155 @@ pub mod migrations { pallet_staking::migrations::v15::MigrateV14ToV15, parachains_inclusion::migration::MigrateToV1, parachains_assigner_on_demand::migration::MigrateV0ToV1, + restore_corrupted_ledgers::Migrate, ); /// Migrations/checks that do not need to be versioned and can run on every update. pub type Permanent = (pallet_xcm::migration::MigrateToLatestXcmVersion,); } +/// Migration to fix current corrupted staking ledgers in Kusama. +/// +/// It consists of: +/// * Call into `pallet_staking::Pallet::::restore_ledger` with: +/// * Root origin; +/// * Default `None` paramters. +/// * Forces unstake of recovered ledger if the final restored ledger has higher stake than the +/// stash's free balance. +/// +/// The stashes associated with corrupted ledgers that will be "migrated" are set in +/// [`CorruptedStashes`]. +/// +/// For more details about the corrupt ledgers issue, recovery and which stashes to migrate, check +/// . +pub(crate) mod restore_corrupted_ledgers { + use super::*; + + use frame_support::traits::{Currency, OnRuntimeUpgrade}; + use frame_system::RawOrigin; + + use pallet_staking::WeightInfo; + use sp_staking::StakingAccount; + + parameter_types! { + pub CorruptedStashes: Vec = vec![ + // stash account ESGsxFePah1qb96ooTU4QJMxMKUG7NZvgTig3eJxP9f3wLa + hex_literal::hex!["52559f2c7324385aade778eca4d7837c7492d92ee79b66d6b416373066869d2e"].into(), + // stash account DggTJdwWEbPS4gERc3SRQL4heQufMeayrZGDpjHNC1iEiui + hex_literal::hex!["31162f413661f3f5351169299728ab6139725696ac6f98db9665e8b76d73d300"].into(), + // stash account Du2LiHk1D1kAoaQ8wsx5jiNEG5CNRQEg6xME5iYtGkeQAJP + hex_literal::hex!["3a8012a52ec2715e711b1811f87684fe6646fc97a276043da7e75cd6a6516d29"].into(), + ]; + } + + pub struct Migrate(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for Migrate { + fn on_runtime_upgrade() -> Weight { + let mut total_weight: Weight = Default::default(); + let mut ok_migration = 0; + let mut err_migration = 0; + + for stash in CorruptedStashes::get().into_iter() { + let stash_account: T::AccountId = if let Ok(stash_account) = + T::AccountId::decode(&mut &Into::<[u8; 32]>::into(stash.clone())[..]) + { + stash_account + } else { + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: error converting account {:?}. skipping.", + stash.clone(), + ); + err_migration += 1; + continue + }; + + // restore currupted ledger. + match pallet_staking::Pallet::::restore_ledger( + RawOrigin::Root.into(), + stash_account.clone(), + None, + None, + None, + ) { + Ok(_) => (), // proceed. + Err(err) => { + // note: after first migration run, restoring ledger will fail with + // `staking::pallet::Error::CannotRestoreLedger`. + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: error restoring ledger {:?}, unexpected (unless running try-state idempotency round).", + err + ); + continue + }, + }; + + // check if restored ledger total is higher than the stash's free balance. If + // that's the case, force unstake the ledger. + let weight = if let Ok(ledger) = pallet_staking::Pallet::::ledger( + StakingAccount::Stash(stash_account.clone()), + ) { + // force unstake the ledger. + if ledger.total > T::Currency::free_balance(&stash_account) { + let slashing_spans = 10; // default slashing spans for migration. + let _ = pallet_staking::Pallet::::force_unstake( + RawOrigin::Root.into(), + stash_account.clone(), + slashing_spans, + ) + .map_err(|err| { + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: error force unstaking ledger, unexpected. {:?}", + err + ); + err_migration += 1; + err + }); + + log::info!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: ledger of {:?} restored (with force unstake).", + stash_account, + ); + ok_migration += 1; + + ::restore_ledger() + .saturating_add(::force_unstake(slashing_spans)) + } else { + log::info!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: ledger of {:?} restored.", + stash, + ); + ok_migration += 1; + + ::restore_ledger() + } + } else { + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: ledger does not exist, unexpected." + ); + err_migration += 1; + ::restore_ledger() + }; + + total_weight.saturating_accrue(weight); + } + + log::info!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: done. success: {}, error: {}", + ok_migration, err_migration + ); + + total_weight + } + } +} + /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; diff --git a/relay/polkadot/Cargo.toml b/relay/polkadot/Cargo.toml index e4fe856855..4cad587ce0 100644 --- a/relay/polkadot/Cargo.toml +++ b/relay/polkadot/Cargo.toml @@ -93,6 +93,7 @@ pallet-election-provider-support-benchmarking = { optional = true, workspace = t pallet-offences-benchmarking = { optional = true, workspace = true } pallet-session-benchmarking = { optional = true, workspace = true } pallet-nomination-pools-benchmarking = { optional = true, workspace = true } +hex-literal = { workspace = true } polkadot-runtime-common = { workspace = true } runtime-parachains = { workspace = true } diff --git a/relay/polkadot/src/lib.rs b/relay/polkadot/src/lib.rs index 1a7c8958f7..f61798679e 100644 --- a/relay/polkadot/src/lib.rs +++ b/relay/polkadot/src/lib.rs @@ -2056,12 +2056,158 @@ pub mod migrations { GetLegacyLeaseImpl, >, CancelAuctions, + restore_corrupted_ledgers::Migrate, ); /// Migrations/checks that do not need to be versioned and can run on every update. pub type Permanent = (pallet_xcm::migration::MigrateToLatestXcmVersion,); } +/// Migration to fix current corrupted staking ledgers in Polkadot. +/// +/// It consists of: +/// * Call into `pallet_staking::Pallet::::restore_ledger` with: +/// * Root origin; +/// * Default `None` paramters. +/// * Forces unstake of recovered ledger if the final restored ledger has higher stake than the +/// stash's free balance. +/// +/// The stashes associated with corrupted ledgers that will be "migrated" are set in +/// [`CorruptedStashes`]. +/// +/// For more details about the corrupt ledgers issue, recovery and which stashes to migrate, check +/// . +pub(crate) mod restore_corrupted_ledgers { + use super::*; + + use frame_support::traits::Currency; + use frame_system::RawOrigin; + + use pallet_staking::WeightInfo; + use sp_staking::StakingAccount; + + parameter_types! { + pub CorruptedStashes: Vec = vec![ + // stash account 138fZsNu67JFtiiWc1eWK2Ev5jCYT6ZirZM288tf99CUHk8K + hex_literal::hex!["5e510306a89f40e5520ae46adcc7a4a1bbacf27c86c163b0691bbbd7b5ef9c10"].into(), + // stash account 14kwUJW6rtjTVW3RusMecvTfDqjEMAt8W159jAGBJqPrwwvC + hex_literal::hex!["a6379e16c5dab15e384c71024e3c6667356a5487127c291e61eed3d8d6b335dd"].into(), + // stash account 13SvkXXNbFJ74pHDrkEnUw6AE8TVkLRRkUm2CMXsQtd4ibwq + hex_literal::hex!["6c3e8acb9225c2a6d22539e2c268c8721b016be1558b4aad4bed220dfbf01fea"].into(), + // stash account 12YcbjN5cvqM63oK7WMhNtpTQhtCrrUr4ntzqqrJ4EijvDE8 + hex_literal::hex!["4458ad5f0c082da64610607beb9d3164a77f1ef7964b7871c1de182ea7213783"].into(), + ]; + } + + pub struct Migrate(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for Migrate { + fn on_runtime_upgrade() -> Weight { + let mut total_weight: Weight = Default::default(); + let mut ok_migration = 0; + let mut err_migration = 0; + + for stash in CorruptedStashes::get().into_iter() { + let stash_account: T::AccountId = if let Ok(stash_account) = + T::AccountId::decode(&mut &Into::<[u8; 32]>::into(stash.clone())[..]) + { + stash_account + } else { + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: error converting account {:?}. skipping.", + stash.clone(), + ); + err_migration += 1; + continue + }; + + // restore currupted ledger. + match pallet_staking::Pallet::::restore_ledger( + RawOrigin::Root.into(), + stash_account.clone(), + None, + None, + None, + ) { + Ok(_) => (), // proceed. + Err(err) => { + // note: after first migration run, restoring ledger will fail with + // `staking::pallet::Error::CannotRestoreLedger`. + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: error restoring ledger {:?}, unexpected (unless running try-state idempotency round).", + err + ); + continue + }, + }; + + // check if restored ledger total is higher than the stash's free balance. If + // that's the case, force unstake the ledger. + let weight = if let Ok(ledger) = pallet_staking::Pallet::::ledger( + StakingAccount::Stash(stash_account.clone()), + ) { + // force unstake the ledger. + if ledger.total > T::Currency::free_balance(&stash_account) { + let slashing_spans = 10; // default slashing spans for migration. + let _ = pallet_staking::Pallet::::force_unstake( + RawOrigin::Root.into(), + stash_account.clone(), + slashing_spans, + ) + .map_err(|err| { + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: error force unstaking ledger, unexpected. {:?}", + err + ); + err_migration += 1; + err + }); + + log::info!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: ledger of {:?} restored (with force unstake).", + stash_account, + ); + ok_migration += 1; + + ::restore_ledger() + .saturating_add(::force_unstake(slashing_spans)) + } else { + log::info!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: ledger of {:?} restored.", + stash, + ); + ok_migration += 1; + + ::restore_ledger() + } + } else { + log::error!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: ledger does not exist, unexpected." + ); + err_migration += 1; + ::restore_ledger() + }; + + total_weight.saturating_accrue(weight); + } + + log::info!( + target: LOG_TARGET, + "migrations::corrupted_ledgers: done. success: {}, error: {}", + ok_migration, + err_migration + ); + + total_weight + } + } +} + /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic;