Skip to content

Commit

Permalink
Refactors staking and stake-tracker so that target and voter lists …
Browse files Browse the repository at this point in the history
…include idle stakers (#2497)

In this PR we change the current assumptions so that `TargetList` and
`VoterList` **also** keep track of the idle stakers (w.g. chilled) and
the target scores are tracked and updated even while the validator is
chilled. The idle stakers will be removed when the ledger unbonds.

This allows the stake-tracker to keep track of the chilled validators
and respective score so that a re-validate of a chilled validator is
brought up with the correct target score (i.e. self stake + all current
nominations in the system).

---------

Co-authored-by: command-bot <>
  • Loading branch information
gpestana authored Dec 2, 2023
1 parent 25c97e1 commit f9c439c
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 101 deletions.
9 changes: 7 additions & 2 deletions substrate/frame/staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ impl ExtBuilder {
let mut ext = self.build();
ext.execute_with(test);
ext.execute_with(|| {
Staking::do_try_state(System::block_number())
let _ = Staking::do_try_state(System::block_number())
.map_err(|err| println!(" 🕵️‍♂️ try_state failure: {:?}", err))
.unwrap();
});
Expand Down Expand Up @@ -881,7 +881,12 @@ pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) {
pub(crate) fn stake_tracker_sanity_tests() -> Result<(), &'static str> {
use sp_staking::StakingInterface;

assert_eq!(Nominators::<Test>::count() + Validators::<Test>::count(), VoterBagsList::count());
assert_eq!(
Nominators::<Test>::count() + Validators::<Test>::count(),
VoterBagsList::iter()
.filter(|v| Staking::status(&v) != Ok(StakerStatus::Idle))
.count() as u32,
);

// recalculate the target's stake based on voter's nominations and compare with the score in the
// target list.
Expand Down
178 changes: 133 additions & 45 deletions substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,10 @@ impl<T: Config> Pallet<T> {

/// Chill a stash account.
pub(crate) fn chill_stash(stash: &T::AccountId) {
let chilled_as_validator = Self::do_remove_validator(stash);
let chilled_as_nominator = Self::do_remove_nominator(stash);
let chilled_as_validator = Self::do_chill_validator(stash);
let chilled_as_nominator = Self::do_chill_nominator(stash);
if chilled_as_validator || chilled_as_nominator {
debug_assert_eq!(Self::status(stash), Ok(StakerStatus::Idle));
Self::deposit_event(Event::<T>::Chilled { stash: stash.clone() });
}
}
Expand Down Expand Up @@ -832,7 +833,9 @@ impl<T: Config> Pallet<T> {
let mut nominators_taken = 0u32;
let mut min_active_stake = u64::MAX;

let mut sorted_voters = T::VoterList::iter();
// voter list also contains chilled/idle voters, filter those.
let mut sorted_voters =
T::VoterList::iter().filter(|v| Self::status(&v) != Ok(StakerStatus::Idle));
while all_voters.len() < final_predicted_len as usize &&
voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{
Expand Down Expand Up @@ -940,7 +943,9 @@ impl<T: Config> Pallet<T> {
let mut all_targets = Vec::<T::AccountId>::with_capacity(final_predicted_len as usize);
let mut targets_seen = 0;

let mut targets_iter = T::TargetList::iter();
// target list also contains chilled/idle validators, filter those.
let mut targets_iter =
T::TargetList::iter().filter(|t| Self::status(&t) != Ok(StakerStatus::Idle));
while all_targets.len() < final_predicted_len as usize &&
targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
{
Expand Down Expand Up @@ -980,28 +985,52 @@ impl<T: Config> Pallet<T> {
/// to `Nominators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_add_nominator(who: &T::AccountId, nominations: Nominations<T>) {
if !Nominators::<T>::contains_key(who) {
// new nominator.
Nominators::<T>::insert(who, nominations);
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_add(
who,
);
} else {
// update nominations.
let prev_nominations = Self::nominations(who).unwrap_or_default();
Nominators::<T>::insert(who, nominations);
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_update(
who,
prev_nominations,
);
}
match (Nominators::<T>::contains_key(who), T::VoterList::contains(who)) {
(false, false) => {
// new nomination
Nominators::<T>::insert(who, nominations);
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_add(
who,
);
},
(true, true) | (false, true) => {
// update nominations or un-chill nominator.
let prev_nominations = Self::nominations(who).unwrap_or_default();
Nominators::<T>::insert(who, nominations);
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_update(
who,
prev_nominations,
);
},
(true, false) => {
defensive!("unexpected state.");
},
};

debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
T::VoterList::iter()
.filter(|v| Self::status(&v) != Ok(StakerStatus::Idle))
.count() as u32,
);
}

pub(crate) fn do_chill_nominator(who: &T::AccountId) -> bool {
let outcome = if Nominators::<T>::contains_key(who) {
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_idle(
who,
Self::nominations(who).unwrap_or_default(),
);

Nominators::<T>::remove(who);
true
} else {
false
};

outcome
}

/// This function will remove a nominator from the `Nominators` storage map,
/// and `VoterList`.
///
Expand All @@ -1011,20 +1040,44 @@ impl<T: Config> Pallet<T> {
/// `Nominators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_remove_nominator(who: &T::AccountId) -> bool {
let outcome = if Nominators::<T>::contains_key(who) {
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_remove(
who,
Self::nominations(who).unwrap_or_default(),
);
Nominators::<T>::remove(who);
true
} else {
false
let nominations = Self::nominations(who).unwrap_or_default();

let outcome = match (Nominators::<T>::contains_key(who), T::VoterList::contains(who)) {
// ensure that the voter is idle before removing it.
(true, true) => {
// first chill nominator.
Self::do_chill_nominator(who);

<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_remove(
who,
nominations,
);
Nominators::<T>::remove(who);
true
},
// nominator is idle already, remove it.
(false, true) => {
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_nominator_remove(
who,
nominations,
);
true
},
(true, false) => {
defensive!(
"inconsistent state: staker is in nominators list but not in voter list"
);
false
},
// nominator has been already removed.
(false, false) => false,
};

debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
T::VoterList::iter()
.filter(|v| Self::status(&v) != Ok(StakerStatus::Idle))
.count() as u32,
);

outcome
Expand All @@ -1049,10 +1102,21 @@ impl<T: Config> Pallet<T> {

debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
T::VoterList::iter()
.filter(|v| Self::status(&v) != Ok(StakerStatus::Idle))
.count() as u32,
);
}

pub(crate) fn do_chill_validator(who: &T::AccountId) -> bool {
if Validators::<T>::contains_key(who) {
Validators::<T>::remove(who);
true
} else {
false
}
}

/// This function will remove a validator from the `Validators` storage map.
///
/// Returns true if `who` was removed from `Validators`, otherwise false.
Expand All @@ -1061,19 +1125,39 @@ impl<T: Config> Pallet<T> {
/// `Validators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_remove_validator(who: &T::AccountId) -> bool {
let outcome = if Validators::<T>::contains_key(who) {
Validators::<T>::remove(who);
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_validator_remove(
who,
);
true
} else {
false
let outcome = match (Validators::<T>::contains_key(who), T::TargetList::contains(who)) {
// ensure the validator is idle before removing it.
(true, true) => {
Self::do_chill_validator(who);

<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_validator_remove(
who,
);
Validators::<T>::remove(who);
true
},
// validator is idle, remove it.
(false, true) => {
<T::EventListeners as OnStakingUpdate<T::AccountId, BalanceOf<T>>>::on_validator_remove(
who,
);
true
},
(true, false) => {
defensive!(
"inconsistent state: staker is in validators list but not in targets list"
);
false
},
// validator has been already removed.
(false, false) => false,
};

debug_assert_eq!(
Nominators::<T>::count() + Validators::<T>::count(),
T::VoterList::count()
T::VoterList::iter()
.filter(|v| Self::status(&v) != Ok(StakerStatus::Idle))
.count() as u32,
);

outcome
Expand Down Expand Up @@ -1824,8 +1908,9 @@ impl<T: Config> StakingInterface for Pallet<T> {
impl<T: Config> Pallet<T> {
pub(crate) fn do_try_state(_: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
ensure!(
T::VoterList::iter()
.all(|x| <Nominators<T>>::contains_key(&x) || <Validators<T>>::contains_key(&x)),
T::VoterList::iter().all(|x| <Nominators<T>>::contains_key(&x) ||
<Validators<T>>::contains_key(&x) ||
Self::status(&x) == Ok(StakerStatus::Idle)),
"VoterList contains non-staker"
);

Expand All @@ -1837,12 +1922,15 @@ impl<T: Config> Pallet<T> {

fn check_count() -> Result<(), TryRuntimeError> {
ensure!(
<T as Config>::VoterList::count() ==
Nominators::<T>::count() + Validators::<T>::count(),
<T as Config>::VoterList::iter()
.filter(|v| Self::status(&v) != Ok(StakerStatus::Idle))
.count() as u32 == Nominators::<T>::count() + Validators::<T>::count(),
"wrong external count (VoterList.count != Nominators.count + Validators.count)"
);
ensure!(
<T as Config>::TargetList::count() == Validators::<T>::count(),
<T as Config>::TargetList::iter()
.filter(|t| Self::status(&t) != Ok(StakerStatus::Idle))
.count() as u32 == Validators::<T>::count(),
"wrong external count (TargetList.count != Validators.count)"
);
ensure!(
Expand Down
5 changes: 3 additions & 2 deletions substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,7 @@ pub mod pallet {
}
}

Self::do_remove_nominator(stash);
Self::do_chill_nominator(stash);
Self::do_add_validator(stash, prefs.clone());
Self::deposit_event(Event::<T>::ValidatorPrefsSet { stash: ledger.stash, prefs });

Expand Down Expand Up @@ -1225,7 +1225,7 @@ pub mod pallet {
suppressed: false,
};

Self::do_remove_validator(stash);
Self::do_chill_validator(stash);
Self::do_add_nominator(stash, nominations);

Ok(())
Expand Down Expand Up @@ -1646,6 +1646,7 @@ pub mod pallet {
if let Some(ref mut nom) = maybe_nom {
if let Some(pos) = nom.targets.iter().position(|v| v == stash) {
nom.targets.swap_remove(pos);

// update nominations and trickle down to target list score.
Self::do_add_nominator(&nom_stash, nom.clone());

Expand Down
Loading

0 comments on commit f9c439c

Please sign in to comment.