Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

[fix] Bound staking ledger correctly with MaxUnlockingChunks from configuration #12343

Merged
merged 14 commits into from
Sep 27, 2022
4 changes: 2 additions & 2 deletions frame/staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ benchmarks! {
}

rebond {
let l in 1 .. MaxUnlockingChunks::get() as u32;
let l in 1 .. T::MaxUnlockingChunks::get() as u32;

// clean up any existing state.
clear_validators_and_nominators::<T>();
Expand Down Expand Up @@ -764,7 +764,7 @@ benchmarks! {

#[extra]
do_slash {
let l in 1 .. MaxUnlockingChunks::get() as u32;
let l in 1 .. T::MaxUnlockingChunks::get() as u32;
let (stash, controller) = create_stash_controller::<T>(0, 100, Default::default())?;
let mut staking_ledger = Ledger::<T>::get(controller.clone()).unwrap();
let unlock_chunk = UnlockChunk::<BalanceOf<T>> {
Expand Down
7 changes: 1 addition & 6 deletions frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ mod pallet;

use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use frame_support::{
parameter_types,
traits::{Currency, Defensive, Get},
weights::Weight,
BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
Expand Down Expand Up @@ -349,10 +348,6 @@ type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<

type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;

parameter_types! {
pub MaxUnlockingChunks: u32 = 32;
}

/// Information regarding the active era (era in used in session).
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ActiveEraInfo {
Expand Down Expand Up @@ -465,7 +460,7 @@ pub struct StakingLedger<T: Config> {
/// Any balance that is becoming free, which may eventually be transferred out of the stash
/// (assuming it doesn't get slashed first). It is assumed that this will be treated as a first
/// in, first out queue where the new (higher value) eras get pushed on the back.
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, MaxUnlockingChunks>,
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
/// List of eras for which the stakers behind a validator have claimed rewards. Only updated
/// for validators.
pub claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>,
Expand Down
3 changes: 2 additions & 1 deletion frame/staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ parameter_types! {
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
pub static MaxNominations: u32 = 16;
pub static HistoryDepth: u32 = 80;
pub static MaxUnlockingChunks: u32 = 32;
pub static RewardOnUnbalanceWasCalled: bool = false;
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
}
Expand Down Expand Up @@ -301,7 +302,7 @@ impl crate::pallet::pallet::Config for Test {
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = VoterBagsList;
type TargetList = UseValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type MaxUnlockingChunks = MaxUnlockingChunks;
type HistoryDepth = HistoryDepth;
type OnStakerSlash = OnStakerSlashMock<Test>;
type BenchmarkingConfig = TestBenchmarkingConfig;
Expand Down
27 changes: 18 additions & 9 deletions frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ pub use impls::*;

use crate::{
slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout,
EraRewardPoints, Exposure, Forcing, MaxUnlockingChunks, NegativeImbalanceOf, Nominations,
PositiveImbalanceOf, Releases, RewardDestination, SessionInterface, StakingLedger,
UnappliedSlash, UnlockChunk, ValidatorPrefs,
EraRewardPoints, Exposure, Forcing, NegativeImbalanceOf, Nominations, PositiveImbalanceOf,
Releases, RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk,
ValidatorPrefs,
};

const STAKING_ID: LockIdentifier = *b"staking ";
Expand Down Expand Up @@ -142,8 +142,9 @@ pub mod pallet {
///
/// Note: `HistoryDepth` is used as the upper bound for the `BoundedVec`
/// item `StakingLedger.claimed_rewards`. Setting this value lower than
/// the existing value can lead to inconsistencies and will need to be
/// handled properly in a migration.
/// the existing value can lead to inconsistencies in the
/// `StakingLedger` and will need to be handled properly in a migration.
/// The test `reducing_history_depth_abrupt` shows this effect.
#[pallet::constant]
type HistoryDepth: Get<u32>;

Expand Down Expand Up @@ -237,8 +238,16 @@ pub mod pallet {
/// VALIDATOR.
type TargetList: SortedListProvider<Self::AccountId, Score = BalanceOf<Self>>;

/// The maximum number of `unlocking` chunks a [`StakingLedger`] can have. Effectively
/// determines how many unique eras a staker may be unbonding in.
/// The maximum number of `unlocking` chunks a [`StakingLedger`] can
/// have. Effectively determines how many unique eras a staker may be
/// unbonding in.
///
/// Note: `MaxUnlockingChunks` is used as the upper bound for the
/// `BoundedVec` item `StakingLedger.unlocking`. Setting this value
/// lower than the existing value can lead to inconsistencies in the
/// `StakingLedger` and will need to be handled properly in a runtime
/// migration. The test `reducing_max_unlocking_chunks_abrupt` shows
/// this effect.
#[pallet::constant]
type MaxUnlockingChunks: Get<u32>;

Expand Down Expand Up @@ -943,7 +952,7 @@ pub mod pallet {
let controller = ensure_signed(origin)?;
let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
ensure!(
ledger.unlocking.len() < MaxUnlockingChunks::get() as usize,
ledger.unlocking.len() < T::MaxUnlockingChunks::get() as usize,
Error::<T>::NoMoreChunks,
);

Expand Down Expand Up @@ -1457,7 +1466,7 @@ pub mod pallet {
/// - Bounded by `MaxUnlockingChunks`.
/// - Storage changes: Can't increase storage, only decrease it.
/// # </weight>
#[pallet::weight(T::WeightInfo::rebond(MaxUnlockingChunks::get() as u32))]
#[pallet::weight(T::WeightInfo::rebond(T::MaxUnlockingChunks::get() as u32))]
pub fn rebond(
origin: OriginFor<T>,
#[pallet::compact] value: BalanceOf<T>,
Expand Down
61 changes: 57 additions & 4 deletions frame/staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

//! Tests for the module.

use super::{ConfigOp, Event, MaxUnlockingChunks, *};
use super::{ConfigOp, Event, *};
use frame_election_provider_support::{ElectionProvider, SortedListProvider, Support};
use frame_support::{
assert_noop, assert_ok, assert_storage_noop, bounded_vec,
Expand Down Expand Up @@ -1346,7 +1346,8 @@ fn too_many_unbond_calls_should_not_work() {
ExtBuilder::default().build_and_execute(|| {
let mut current_era = 0;
// locked at era MaxUnlockingChunks - 1 until 3
for i in 0..MaxUnlockingChunks::get() - 1 {

for i in 0..<<Test as Config>::MaxUnlockingChunks as Get<u32>>::get() - 1 {
// There is only 1 chunk per era, so we need to be in a new era to create a chunk.
current_era = i as u32;
mock::start_active_era(current_era);
Expand All @@ -1361,7 +1362,7 @@ fn too_many_unbond_calls_should_not_work() {
assert_ok!(Staking::unbond(RuntimeOrigin::signed(10), 1));
assert_eq!(
Staking::ledger(&10).unwrap().unlocking.len(),
MaxUnlockingChunks::get() as usize
<<Test as Config>::MaxUnlockingChunks as Get<u32>>::get() as usize
);
// can't do more.
assert_noop!(Staking::unbond(RuntimeOrigin::signed(10), 1), Error::<Test>::NoMoreChunks);
Expand Down Expand Up @@ -5483,7 +5484,7 @@ fn pre_bonding_era_cannot_be_claimed() {
}

#[test]
fn reducing_history_depth_without_migration() {
fn reducing_history_depth_abrupt() {
// Verifies initial conditions of mock
ExtBuilder::default().nominate(false).build_and_execute(|| {
let original_history_depth = HistoryDepth::get();
Expand Down Expand Up @@ -5560,3 +5561,55 @@ fn reducing_history_depth_without_migration() {
HistoryDepth::set(original_history_depth);
});
}

#[test]
fn reducing_max_unlocking_chunks_abrupt() {
// Concern is on validators only
// By Default 11, 10 are stash and ctrl and 21,20
ExtBuilder::default().build_and_execute(|| {
// given a staker at era=10 and MaxUnlockChunks set to 2
MaxUnlockingChunks::set(2);
start_active_era(10);
assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 4, 300, RewardDestination::Staked));
assert!(matches!(Staking::ledger(4), Some(_)));

// when staker unbonds
assert_ok!(Staking::unbond(RuntimeOrigin::signed(4), 20));

// then an unlocking chunk is added at `current_era + bonding_duration`
// => 10 + 3 = 13
let expected_unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks> =
bounded_vec![UnlockChunk { value: 20 as Balance, era: 13 as EraIndex }];
assert!(matches!(Staking::ledger(4),
Some(StakingLedger {
unlocking,
..
}) if unlocking==expected_unlocking));

// when staker unbonds at next era
start_active_era(11);
assert_ok!(Staking::unbond(RuntimeOrigin::signed(4), 50));
// then another unlock chunk is added
let expected_unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks> =
bounded_vec![UnlockChunk { value: 20, era: 13 }, UnlockChunk { value: 50, era: 14 }];
assert!(matches!(Staking::ledger(4),
Some(StakingLedger {
unlocking,
..
}) if unlocking==expected_unlocking));

// when staker unbonds further
start_active_era(12);
// then further unbonding not possible
assert_noop!(Staking::unbond(RuntimeOrigin::signed(4), 20), Error::<Test>::NoMoreChunks);

// when max unlocking chunks is reduced abruptly to a low value
MaxUnlockingChunks::set(1);
// then unbond, rebond ops are blocked with ledger in corrupt state
assert_noop!(Staking::unbond(RuntimeOrigin::signed(4), 20), Error::<Test>::NotController);
assert_noop!(Staking::rebond(RuntimeOrigin::signed(4), 100), Error::<Test>::NotController);

// reset the ledger corruption
MaxUnlockingChunks::set(2);
})
}