-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Extrinsic to restore corrupt staking ledgers #3706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
03d821c
68df5d2
fc57112
4038ca1
af010de
87ad1e5
03cf845
9eea773
fbbf7f8
64cbee1
a6cb392
8f52d0a
c3ed2c3
11920e4
42525dc
c9aba77
ae4a192
df0cf74
a57a699
e0bcbd3
209f3b0
b3e4a73
42bb49d
7099763
aaba944
5da7bc8
61eb993
5f04bb9
46360fe
2ed2ed6
9426202
10fc869
f27ef5b
a3100f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 | ||
| # See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json | ||
|
|
||
| title: Extrinsic to restore corrupted staking ledgers | ||
|
|
||
| doc: | ||
| - audience: Runtime User | ||
| description: | | ||
| This PR adds a new extrinsic `Call::restore_ledger ` gated by `StakingAdmin` origin that restores a corrupted staking ledger. This extrinsic will be used to recover ledgers that were affected by the issue discussed in https://github.com/paritytech/polkadot-sdk/issues/3245. | ||
| The extrinsic will re-write the storage items associated with a stash account provided as input parameter. The data used to reset the ledger can be either i) fetched on-chain or ii) partially/totally set by the input parameters of the call. | ||
|
|
||
| Changes introduced: | ||
| - Adds `Call::restore_ledger ` extrinsic to recover a corrupted ledger; | ||
| - Adds trait `frame_support::traits::currency::InspectLockableCurrency` to allow external pallets to read current locks given an account and lock ID; | ||
| - Implements the `InspectLockableCurrency` in the pallet-balances. | ||
| - Adds staking locks try-runtime checks (https://github.com/paritytech/polkadot-sdk/issues/3751) | ||
|
|
||
| crates: | ||
| - name: pallet-staking | ||
| - name: pallet-balances |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,8 +27,8 @@ use frame_support::{ | |
| dispatch::WithPostDispatchInfo, | ||
| pallet_prelude::*, | ||
| traits::{ | ||
| Currency, Defensive, DefensiveSaturating, EstimateNextNewSession, Get, Imbalance, Len, | ||
| OnUnbalanced, TryCollect, UnixTime, | ||
| Currency, Defensive, DefensiveSaturating, EstimateNextNewSession, Get, Imbalance, | ||
| InspectLockableCurrency, Len, OnUnbalanced, TryCollect, UnixTime, | ||
| }, | ||
| weights::Weight, | ||
| }; | ||
|
|
@@ -50,8 +50,8 @@ use sp_std::prelude::*; | |
| use crate::{ | ||
| election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, | ||
| BalanceOf, EraInfo, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure, | ||
| MaxNominationsOf, MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, | ||
| RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs, | ||
| LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, Nominations, NominationsQuota, | ||
| PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs, | ||
| }; | ||
|
|
||
| use super::pallet::*; | ||
|
|
@@ -84,6 +84,38 @@ impl<T: Config> Pallet<T> { | |
| StakingLedger::<T>::paired_account(Stash(stash.clone())) | ||
| } | ||
|
|
||
| /// Inspects and returns the corruption state of a ledger and bond, if any. | ||
| /// | ||
| /// Note: all operations in this method access directly the `Bonded` and `Ledger` storage maps | ||
| /// instead of using the [`StakingLedger`] API since the bond and/or ledger may be corrupted. | ||
| pub(crate) fn inspect_bond_state( | ||
| stash: &T::AccountId, | ||
| ) -> Result<LedgerIntegrityState, Error<T>> { | ||
| let lock = T::Currency::balance_locked(crate::STAKING_ID, &stash); | ||
|
|
||
| let controller = <Bonded<T>>::get(stash).ok_or_else(|| { | ||
| if lock == Zero::zero() { | ||
| Error::<T>::NotStash | ||
| } else { | ||
| Error::<T>::BadState | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could return something like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and this branch should be defensive as in to the best of our knowledge, no ledger should be in this error state right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We actually may get into this state when the stash associated with a corrupted ledger calls kill on the ledger, this specific case is explained here: https://hackmd.io/DLb5jEYWSmmvqXC9ae4yRg#/5 |
||
| } | ||
| })?; | ||
|
|
||
| match Ledger::<T>::get(controller) { | ||
| Some(ledger) => | ||
| if ledger.stash != *stash { | ||
| Ok(LedgerIntegrityState::Corrupted) | ||
Ank4n marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| if lock != ledger.total { | ||
| Ok(LedgerIntegrityState::LockCorrupted) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A ledger can also have both lock out of sync and stash mismatch?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the stash is a mismatch, it means it is someone else's ledger, so I think it is kinda meaningless to even check And I think we only have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, it can. If there's a bond_extra on a corrupted ledger, we may get that issue (see e.g. this case). The reason to differentiate between We could also collapse both and return an error if the total to reset the ledger is 0, ie. let new_total = maybe_total.unwrap_or(lock);
ensure!(new_total != 0, Error::<T>::CannotResetLedger); |
||
| } else { | ||
| Ok(LedgerIntegrityState::Ok) | ||
| } | ||
| }, | ||
| None => Ok(LedgerIntegrityState::CorruptedKilled), | ||
| } | ||
| } | ||
|
|
||
| /// The total balance that can be slashed from a stash account as of right now. | ||
| pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> { | ||
| // Weight note: consider making the stake accessible through stash. | ||
|
|
@@ -1837,12 +1869,12 @@ impl<T: Config> Pallet<T> { | |
| "VoterList contains non-staker" | ||
| ); | ||
|
|
||
| Self::check_ledgers()?; | ||
| Self::check_bonded_consistency()?; | ||
| Self::check_payees()?; | ||
| Self::check_nominators()?; | ||
| Self::check_exposures()?; | ||
| Self::check_paged_exposures()?; | ||
| Self::check_ledgers()?; | ||
| Self::check_count() | ||
| } | ||
|
|
||
|
|
@@ -1851,6 +1883,7 @@ impl<T: Config> Pallet<T> { | |
| /// * A bonded (stash, controller) pair should have only one associated ledger. I.e. if the | ||
| /// ledger is bonded by stash, the controller account must not bond a different ledger. | ||
| /// * A bonded (stash, controller) pair must have an associated ledger. | ||
| /// | ||
| /// NOTE: these checks result in warnings only. Once | ||
| /// <https://github.com/paritytech/polkadot-sdk/issues/3245> is resolved, turn warns into check | ||
| /// failures. | ||
|
|
@@ -1945,19 +1978,18 @@ impl<T: Config> Pallet<T> { | |
| } | ||
|
|
||
| /// Invariants: | ||
| /// * `ledger.controller` is not stored in the storage (but populated at retrieval). | ||
| /// * Stake consistency: ledger.total == ledger.active + sum(ledger.unlocking). | ||
| /// * The controller keying the ledger and the ledger stash matches the state of the `Bonded` | ||
| /// storage. | ||
| /// * The ledger's controller and stash matches the associated `Bonded` tuple. | ||
| /// * Staking locked funds for every bonded stash should be the same as its ledger's total. | ||
| /// * Staking ledger and bond are not corrupted. | ||
| fn check_ledgers() -> Result<(), TryRuntimeError> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is the running time for this try state check? Don't mind putting this in release for now if it is not super long but want to point out that you are reading some storages multiple times and it can be optimized to be much faster. |
||
| Bonded::<T>::iter() | ||
| .map(|(stash, ctrl)| { | ||
| // `ledger.controller` is never stored in raw storage. | ||
| let raw = Ledger::<T>::get(stash).unwrap_or_else(|| { | ||
| Ledger::<T>::get(ctrl.clone()) | ||
| .expect("try_check: bonded stash/ctrl does not have an associated ledger") | ||
| }); | ||
| ensure!(raw.controller.is_none(), "raw storage controller should be None"); | ||
| // ensure locks consistency. | ||
| ensure!( | ||
| Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok), | ||
| "bond, ledger and/or staking lock inconsistent for a bonded stash." | ||
| ); | ||
|
|
||
| // ensure ledger consistency. | ||
| Self::ensure_ledger_consistent(ctrl) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.