-
Notifications
You must be signed in to change notification settings - Fork 781
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
Staking ledger bonding fixes #3639
Changes from all commits
30b00e3
8b49b43
20a87e2
bb837d2
31cbd74
6bc85bc
16a692e
47f9e25
8ed57de
410d784
86b0ad7
6e16fa4
5986a4e
578a407
8dd4818
52c648e
e3e9672
541037f
0c1bff6
371b0cf
4670659
649bf59
0dde68a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
title: Prevents staking controllers from becoming stashes of different ledgers; Ensures that no ledger in bad state is mutated. | ||
|
||
doc: | ||
- audience: Runtime User | ||
description: | | ||
This PR introduces a fix to the staking logic which prevents an existing controller from bonding as a stash of another ledger, which | ||
lead to staking ledger inconsistencies down the line. In addition, it adds a few (temporary) gates to prevent ledgers that are already | ||
in a bad state from mutating its state. | ||
|
||
In summary: | ||
* Checks if stash is already a controller when calling `Call::bond` and fails if that's the case; | ||
* Ensures that all fetching ledgers from storage are done through the `StakingLedger` API; | ||
* Ensures that a `Error::BadState` is returned if the ledger bonding is in a bad state. This prevents bad ledgers from mutating (e.g. | ||
`bond_extra`, `set_controller`, etc) its state and avoid further data inconsistencies. | ||
* Prevents stashes which are controllers or another ledger from calling `set_controller`, since that may lead to a bad state. | ||
* Adds further try-state runtime checks that check if there are ledgers in a bad state based on their bonded metadata. | ||
|
||
crates: | ||
- name: pallet-staking |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -32,8 +32,8 @@ | |||||
//! state consistency. | ||||||
use frame_support::{ | ||||||
defensive, | ||||||
traits::{LockableCurrency, WithdrawReasons}, | ||||||
defensive, ensure, | ||||||
traits::{Defensive, LockableCurrency, WithdrawReasons}, | ||||||
}; | ||||||
use sp_staking::StakingAccount; | ||||||
use sp_std::prelude::*; | ||||||
|
@@ -106,18 +106,39 @@ impl<T: Config> StakingLedger<T> { | |||||
/// This getter can be called with either a controller or stash account, provided that the | ||||||
/// account is properly wrapped in the respective [`StakingAccount`] variant. This is meant to | ||||||
/// abstract the concept of controller/stash accounts from the caller. | ||||||
/// | ||||||
/// Returns [`Error::BadState`] when a bond is in "bad state". A bond is in a bad state when a | ||||||
/// stash has a controller which is bonding a ledger associated with another stash. | ||||||
pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> { | ||||||
let controller = match account { | ||||||
StakingAccount::Stash(stash) => <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash), | ||||||
StakingAccount::Controller(controller) => Ok(controller), | ||||||
}?; | ||||||
let (stash, controller) = match account.clone() { | ||||||
gpestana marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
StakingAccount::Stash(stash) => | ||||||
(stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?), | ||||||
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.
Suggested change
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. Should this be defensive? The 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. I suggested it to be defensive because if the caller of this function uses But I see what you mean; if someone calls 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. On top of that, the defensives in testing are a bit too strict to work with. There are cases where we test that There are ways to handle this by breaking down and re-org the tests but I've come across this in other instances and wish that it would be easier to assert a defensive and runtime error at the same time and be able to proceed with the test case. I think this could be improved at the FRAME testing level, I'll think more about it. |
||||||
StakingAccount::Controller(controller) => ( | ||||||
Ledger::<T>::get(&controller) | ||||||
.map(|l| l.stash) | ||||||
.ok_or(Error::<T>::NotController)?, | ||||||
kianenigma marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
controller, | ||||||
), | ||||||
}; | ||||||
|
||||||
<Ledger<T>>::get(&controller) | ||||||
let ledger = <Ledger<T>>::get(&controller) | ||||||
.map(|mut ledger| { | ||||||
ledger.controller = Some(controller.clone()); | ||||||
ledger | ||||||
}) | ||||||
.ok_or(Error::<T>::NotController) | ||||||
.ok_or(Error::<T>::NotController)?; | ||||||
|
||||||
// if ledger bond is in a bad state, return error to prevent applying operations that may | ||||||
// further spoil the ledger's state. A bond is in bad state when the bonded controller is | ||||||
// associted with a different ledger (i.e. a ledger with a different stash). | ||||||
// | ||||||
// See </~https://github.com/paritytech/polkadot-sdk/issues/3245> for more details. | ||||||
ensure!( | ||||||
Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash, | ||||||
Error::<T>::BadState | ||||||
); | ||||||
|
||||||
Ok(ledger) | ||||||
} | ||||||
|
||||||
/// Returns the reward destination of a staking ledger, stored in [`Payee`]. | ||||||
|
@@ -201,6 +222,30 @@ impl<T: Config> StakingLedger<T> { | |||||
} | ||||||
} | ||||||
|
||||||
/// Sets the ledger controller to its stash. | ||||||
pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> { | ||||||
let controller = self.controller.as_ref() | ||||||
.defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.") | ||||||
.ok_or(Error::<T>::NotController)?; | ||||||
|
||||||
ensure!(self.stash != *controller, Error::<T>::AlreadyPaired); | ||||||
|
||||||
// check if the ledger's stash is a controller of another ledger. | ||||||
if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) { | ||||||
// there is a ledger bonded by the stash. In this case, the stash of the bonded ledger | ||||||
// should be the same as the ledger's stash. Otherwise fail to prevent data | ||||||
// inconsistencies. See </~https://github.com/paritytech/polkadot-sdk/pull/3639> for more | ||||||
// details. | ||||||
ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState); | ||||||
} | ||||||
Ank4n marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
<Ledger<T>>::remove(&controller); | ||||||
<Ledger<T>>::insert(&self.stash, &self); | ||||||
<Bonded<T>>::insert(&self.stash, &self.stash); | ||||||
|
||||||
Ok(()) | ||||||
} | ||||||
|
||||||
/// Clears all data related to a staking ledger and its bond in both [`Ledger`] and [`Bonded`] | ||||||
/// storage items and updates the stash staking lock. | ||||||
pub(crate) fn kill(stash: &T::AccountId) -> Result<(), Error<T>> { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One issue I feel when reading this function, since it handles both reading by controller and stake, it becomes a little harder to reason about which checks we need for each. What do you think about just splitting these flows.
So internally two functions
get_by_stash
andget_by_controller
that this function calls.Leave that decision to you though. Feel free to merge if you are happy and we probably will do lot more cleanups and refactors once controllers are removed completely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree with this statement. The idea of this fn was to keep the code refactors to a minimal once the controllers were deprecated. So refactoring the code would basically mean removing the whole
StakingAccount
machinery and change the logic of the getter (and others) but the API would remain almost unchanged. I will keep this as-is for now and we can remove simplify it once the controller logic is completely removed.