Skip to content

Commit

Permalink
force unstake the last remaining corrupt ledger (#538)
Browse files Browse the repository at this point in the history
One last staking ledger that due to the bug found last year is in an
inconsistent state.

The Support team is in touch with the owner, and force_staking them is
the preferred way.

Note that this is only an action that we are doing because the root
cause of the inconsistent stake was buggy code in the polkadot runtime,
not any user interaction.

See:
https://forum.polkadot.network/t/recover-corrupted-staking-ledgers-in-polkadot-and-kusama/9796

---------

Co-authored-by: Bastian Köcher <[email protected]>
  • Loading branch information
kianenigma and bkchr authored Jan 23, 2025
1 parent 81d1f57 commit 5099ad4
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 156 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- Fix missing Encointer democracy pallet hook needed for enactment ([polkadot-fellows/runtimes/pull/508](https://github.com/polkadot-fellows/runtimes/pull/508))
- Improve benchmark configuration: fix storage whitelist in benchmarks ([polkadot-fellows/runtimes/pull/525](https://github.com/polkadot-fellows/runtimes/pull/525))
- Unstake the last remaining corrupt ledger ([polkadot-fellows/runtimes/pull/538](https://github.com/polkadot-fellows/runtimes/pull/538))

### Fixed

Expand Down
191 changes: 35 additions & 156 deletions relay/polkadot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1481,30 +1481,6 @@ impl pallet_delegated_staking::Config for Runtime {
type CoreStaking = Staking;
}

pub struct InitiateNominationPools;
impl frame_support::traits::OnRuntimeUpgrade for InitiateNominationPools {
fn on_runtime_upgrade() -> frame_support::weights::Weight {
// we use one as an indicator if this has already been set.
if pallet_nomination_pools::MaxPools::<Runtime>::get().is_none() {
// 5 DOT to join a pool.
pallet_nomination_pools::MinJoinBond::<Runtime>::put(5 * UNITS);
// 100 DOT to create a pool.
pallet_nomination_pools::MinCreateBond::<Runtime>::put(100 * UNITS);

// Initialize with limits for now.
pallet_nomination_pools::MaxPools::<Runtime>::put(0);
pallet_nomination_pools::MaxPoolMembersPerPool::<Runtime>::put(0);
pallet_nomination_pools::MaxPoolMembers::<Runtime>::put(0);

log::info!(target: LOG_TARGET, "pools config initiated 🎉");
<Runtime as frame_system::Config>::DbWeight::get().reads_writes(1, 5)
} else {
log::info!(target: LOG_TARGET, "pools config already initiated 😏");
<Runtime as frame_system::Config>::DbWeight::get().reads(1)
}
}
}

parameter_types! {
// The deposit configuration for the singed migration. Specially if you want to allow any signed account to do the migration (see `SignedFilter`, these deposits should be high)
pub const MigrationSignedDepositPerItem: Balance = CENTS;
Expand Down Expand Up @@ -2013,158 +1989,61 @@ pub mod migrations {
GetLegacyLeaseImpl,
>,
CancelAuctions,
restore_corrupted_ledgers::Migrate<Runtime>,
// Migrate NominationPools to `DelegateStake` adapter.
pallet_nomination_pools::migration::unversioned::DelegationStakeMigration<
Runtime,
MaxPoolsToMigrate,
>,
restore_corrupt_ledger_2::Migrate,
);

/// Migrations/checks that do not need to be versioned and can run on every update.
pub type Permanent = (pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>,);
}

/// Migration to fix current corrupted staking ledgers in Polkadot.
///
/// It consists of:
/// * Call into `pallet_staking::Pallet::<T>::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
/// <https://hackmd.io/m_h9DRutSZaUqCwM9tqZ3g?view>.
pub(crate) mod restore_corrupted_ledgers {
pub mod restore_corrupt_ledger_2 {
use super::*;

use frame_support::traits::Currency;
use frame_system::RawOrigin;

use pallet_staking::WeightInfo;
use sp_staking::StakingAccount;

parameter_types! {
pub CorruptedStashes: Vec<AccountId> = 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<T>(sp_std::marker::PhantomData<T>);
impl<T: pallet_staking::Config> OnRuntimeUpgrade for Migrate<T> {
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::<T>::restore_ledger(
// see https://polkadot.subscan.io/tools/format_transform, this is the public key of
// 12gmcL9eej9jRBFT26vZLF4b7aAe4P9aEYHGHFzJdmf5arPi
pub CorruptStash: AccountId = hex_literal::hex!(
"4a90f8d375290b428e580408f18359f66ef367f0f8d1d56c5d3e002ad29c8e00"
).into();
}
pub struct Migrate;
impl OnRuntimeUpgrade for Migrate {
fn on_runtime_upgrade() -> frame_election_provider_support::Weight {
// ensure this only runs once, in the 1.4.0 release
if System::last_runtime_upgrade_spec_version() < 1_400_000 {
let _ = pallet_staking::Pallet::<Runtime>::force_unstake(
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::<T>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::<T>::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::<T>::force_unstake(
RawOrigin::Root.into(),
stash_account.clone(),
slashing_spans,
)
.inspect_err(|err| {
log::error!(
target: LOG_TARGET,
"migrations::corrupted_ledgers: error force unstaking ledger, unexpected. {:?}",
err
);
err_migration += 1;
});

log::info!(
target: LOG_TARGET,
"migrations::corrupted_ledgers: ledger of {:?} restored (with force unstake).",
stash_account,
);
ok_migration += 1;

<T::WeightInfo>::restore_ledger()
.saturating_add(<T::WeightInfo>::force_unstake(slashing_spans))
} else {
log::info!(
target: LOG_TARGET,
"migrations::corrupted_ledgers: ledger of {:?} restored.",
stash,
);
ok_migration += 1;

<T::WeightInfo>::restore_ledger()
}
} else {
log::error!(
target: LOG_TARGET,
"migrations::corrupted_ledgers: ledger does not exist, unexpected."
);
err_migration += 1;
<T::WeightInfo>::restore_ledger()
};

total_weight.saturating_accrue(weight);
CorruptStash::get(),
0,
);
log::info!(
target: LOG_TARGET,
"migrations: force unstaked {:?}",
CorruptStash::get()
);
<Runtime as pallet_staking::Config>::WeightInfo::force_unstake(0)
} else {
Default::default()
}
}

log::info!(
target: LOG_TARGET,
"migrations::corrupted_ledgers: done. success: {}, error: {}",
ok_migration,
err_migration
);
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
assert!(pallet_staking::Ledger::<Runtime>::get(CorruptStash::get()).is_some());
Ok(Default::default())
}

total_weight
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
assert!(pallet_staking::Ledger::<Runtime>::get(CorruptStash::get()).is_none());
Ok(())
}
}
}
Expand Down

0 comments on commit 5099ad4

Please sign in to comment.