Skip to content
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

[WIP] RFC-0097 Implementation to Decrease Unbonding Time #5715

Draft
wants to merge 44 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6d8dbdd
add new parameters
rossbulat Sep 14, 2024
0e6c596
use `EraIndex` for parameters
rossbulat Sep 14, 2024
3ffe265
define migration for new parameters
rossbulat Sep 14, 2024
3e6ae67
init prdoc
rossbulat Sep 14, 2024
049fe63
Merge branch 'master' into rb-rfc-0097
rossbulat Sep 19, 2024
93ec807
calculate lowest third total stake
rossbulat Sep 19, 2024
b8d4add
add get_min_lowest_third_stake function
rossbulat Sep 19, 2024
e3999cb
add get_quick_unbond_max_unstake function
rossbulat Sep 19, 2024
ff48144
calculate `unbonding_time_delta`
rossbulat Sep 20, 2024
ba05eec
use u128
rossbulat Sep 20, 2024
2f5153a
Merge branch 'master' into rb-rfc-0097
rossbulat Sep 20, 2024
ba18e42
Merge branch 'master' into rb-rfc-0097
rossbulat Sep 23, 2024
1b8dd6a
Merge branch 'master' into rb-rfc-0097
rossbulat Sep 27, 2024
de9f7e3
use UnbondingQueue struct
rossbulat Sep 27, 2024
41391a3
+ `back_of_unbonding_queue_block_number` to `UnbondingQueue`
rossbulat Sep 27, 2024
755205c
calculate unbonding_block_number
rossbulat Sep 27, 2024
257afd0
amend `new_back_of_unbonding_queue_block_number`
rossbulat Oct 2, 2024
ca0b06c
calculate unbond era, not block number
rossbulat Oct 3, 2024
5ad4a0d
rm unneeded config, add comments
rossbulat Oct 3, 2024
00b6658
shorten `calculate_lowest_third_total_stake`
rossbulat Oct 3, 2024
d3a618a
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 3, 2024
4a8ca72
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 5, 2024
bcc45b7
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 8, 2024
eec5010
fmt
rossbulat Oct 8, 2024
0a44f79
+ test placeholders
rossbulat Oct 8, 2024
59c3418
+ `process_unbond_queue_request`
rossbulat Oct 8, 2024
8b0d204
some test boilerplate
rossbulat Oct 8, 2024
4388cc5
Update substrate/frame/staking/src/lib.rs
rossbulat Oct 8, 2024
1d4e9d5
use `UnbondingQueueConfig` as struct
rossbulat Oct 8, 2024
998e899
roll back migration
rossbulat Oct 8, 2024
455fa73
roll back storage version
rossbulat Oct 8, 2024
eebf3ac
add test TODOs
rossbulat Oct 8, 2024
350dc21
remove unneeded annotation
rossbulat Oct 8, 2024
9e0079a
add TODO
rossbulat Oct 8, 2024
0de5a98
make `UnbondQueueParams` OptionQuery & configurable
rossbulat Oct 9, 2024
c547956
update tests
rossbulat Oct 9, 2024
8bcbe8f
amend defaults
rossbulat Oct 9, 2024
e5f5e0b
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 9, 2024
0faa32d
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 11, 2024
fe6cb9c
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 14, 2024
e574883
Merge branch 'master' into rb-rfc-0097
rossbulat Oct 17, 2024
7672a2c
add TODO
rossbulat Nov 17, 2024
f13819a
Merge branch 'rb-rfc-0097' of https://github.com/rossbulat/polkadot-s…
rossbulat Nov 17, 2024
f8e0292
Merge branch 'master' into rb-rfc-0097
rossbulat Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions prdoc/pr_5715.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
title: Introduce flexible unbonding queue to staking.

doc:
- audience: Runtime Dev
description:
Introduces a flexible unbonding mechanism to decrease the time stakers have to wait to
unbond.

crates:
- name: pallet-staking
bump: minor
28 changes: 28 additions & 0 deletions substrate/frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,34 @@ pub struct ActiveEraInfo {
pub start: Option<u64>,
}

/// Parameters of the unbonding queue mechanism.
#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
#[scale_info(skip_type_params(T))]
pub struct UnbondingQueue {
rossbulat marked this conversation as resolved.
Show resolved Hide resolved
/// The share of stake backing the lowest 1/3 of validators that is slashable at any point in
/// time. It offers a trade-off between security and unbonding time.
pub min_slashable_share: Perbill,
/// The minimum unbonding time for an active stake.
pub unbond_period_lower_bound: EraIndex,
/// The maximum possible unbonding time for an active stake.
pub unbond_period_upper_bound: EraIndex,
// The era when all the existing unbonders have unbonded.
pub back_of_unbonding_queue_era: EraIndex,
}

/// Default implementation for `UnbondingQueue`, providing sensible defaults for slashable share and
/// lower and upper bound eras. `back_of_unbonding_queue_era` can be set to zero, and will update
/// upon the first unlock request by a staker.
impl Default for UnbondingQueue {
fn default() -> Self {
Self {
min_slashable_share: Perbill::from_percent(50),
unbond_period_lower_bound: 2,
unbond_period_upper_bound: 28,
back_of_unbonding_queue_era: Zero::zero(),
}
}
}
/// Reward points of an era. Used to split era total payout between validators.
///
/// This points will be used to reward validators and their respective nominators.
Expand Down
30 changes: 30 additions & 0 deletions substrate/frame/staking/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ impl Default for ObsoleteReleases {
#[storage_alias]
type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;

pub mod v16 {
use super::*;

pub struct VersionUncheckedMigrateV15ToV16<T>(core::marker::PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV15ToV16<T> {
fn on_runtime_upgrade() -> Weight {
// Set the default unbonding queue params.
UnbondingQueueParams::<T>::set(Default::default());
rossbulat marked this conversation as resolved.
Show resolved Hide resolved

log!(info, "v16 applied successfully.");
T::DbWeight::get().reads_writes(0, 1)
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
// TODO: implement.
Ok(())
}
}

/// v16: Adds newly added unbond parameters for flexible unbonding queue.
pub type MigrateV15ToV16<T> = frame_support::migrations::VersionedMigration<
15,
16,
VersionUncheckedMigrateV15ToV16<T>,
Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
}

/// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec<u32>`
pub mod v15 {
use super::*;
Expand Down
92 changes: 91 additions & 1 deletion substrate/frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ impl<T: Config> Pallet<T> {

/// Process the output of the election.
///
/// Store staking information for the new planned era
/// Store staking information for the new planned era.
pub fn store_stakers_info(
exposures: BoundedVec<
(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>),
Expand All @@ -709,12 +709,17 @@ impl<T: Config> Pallet<T> {
// Populate elected stash, stakers, exposures, and the snapshot of validator prefs.
let mut total_stake: BalanceOf<T> = Zero::zero();
let mut elected_stashes = Vec::with_capacity(exposures.len());
// Populate exposures by total stake, to be truncated to lowest third.
let mut exposures_total_stake: Vec<(T::AccountId, BalanceOf<T>)> =
Vec::with_capacity(exposures.len());

exposures.into_iter().for_each(|(stash, exposure)| {
// build elected stash
elected_stashes.push(stash.clone());
// accumulate total stake
total_stake = total_stake.saturating_add(exposure.total);
// add this validator's total stake to the lowest third total stake
exposures_total_stake.push((stash.clone(), exposure.total));
// store staker exposure for this era
EraInfo::<T>::set_exposure(new_planned_era, &stash, exposure);
});
Expand All @@ -731,6 +736,9 @@ impl<T: Config> Pallet<T> {
<ErasValidatorPrefs<T>>::insert(&new_planned_era, stash, pref);
}

// Update unbonding queue related storage.
Self::calculate_lowest_third_total_stake(new_planned_era, exposures_total_stake);

if new_planned_era > 0 {
log!(
info,
Expand All @@ -743,6 +751,39 @@ impl<T: Config> Pallet<T> {
elected_stashes
}

/// Calculate the total stake of the lowest third validators and store it for the planned era.
///
/// Removes the stale entry from `EraLowestThirdTotalStake` if it exists.
fn calculate_lowest_third_total_stake(
new_planned_era: EraIndex,
mut exposures: Vec<(T::AccountId, BalanceOf<T>)>,
) {
let undonding_period_upper_bound =
<UnbondingQueueParams<T>>::get().unbond_period_upper_bound;

// Determine the total stake from lowest third of validators and persist for the era.
let eras_to_check: EraIndex = Perbill::from_percent(33) * undonding_period_upper_bound;

// Sort exposure total stake by lowest first, and truncate to lowest third.
exposures.sort_by(|(_, a), (_, b)| b.cmp(&a));
exposures.truncate(eras_to_check.try_into().unwrap_or(Default::default()));

// Calculate the total stake of the lowest third validators.
let total_stake: BalanceOf<T> = exposures
.into_iter()
.map(|(_, a)| a)
.reduce(|a, b| a.saturating_add(b))
.unwrap_or(Default::default());

// Store the total stake of the lowest third validators for the planned era.
EraLowestThirdTotalStake::<T>::insert(new_planned_era, total_stake);

// Remove stale entry from `EraLowestThirdTotalStake`.
<EraLowestThirdTotalStake<T>>::remove(
new_planned_era.saturating_sub(undonding_period_upper_bound),
);
}

/// Consume a set of [`BoundedSupports`] from [`sp_npos_elections`] and collect them into a
/// [`Exposure`].
fn collect_exposures(
Expand Down Expand Up @@ -780,6 +821,55 @@ impl<T: Config> Pallet<T> {
.expect("we only map through support vector which cannot change the size; qed")
}

/// Gets the lowest of the lowest third validator stake entries for the last
/// upper bound eras.
pub(crate) fn get_min_lowest_third_stake(from_era: EraIndex) -> BalanceOf<T> {
let unbonding_queue_params = <UnbondingQueueParams<T>>::get();
let undonding_period_upper_bound = unbonding_queue_params.unbond_period_upper_bound;

// Find the minimum total stake of the lowest third validators over the configured number of
// eras.
let mut eras_checked = 0;
let mut current_era = from_era;
let mut lowest_stake: BalanceOf<T> = Zero::zero();
let last_era = from_era.saturating_sub(undonding_period_upper_bound);

while current_era >= last_era || eras_checked <= undonding_period_upper_bound {
if let Some(lowest_third_total_stake) = <EraLowestThirdTotalStake<T>>::get(current_era)
{
if eras_checked == 0 || lowest_third_total_stake < lowest_stake {
lowest_stake = lowest_third_total_stake;
}
}
current_era = current_era.saturating_sub(1);
eras_checked += 1;
}
lowest_stake
}

// Get the maximum unstake amount for quick unbond time supported at the time of an unbond
// request.
pub fn get_quick_unbond_max_unstake(from_era: EraIndex) -> BalanceOf<T> {
<UnbondingQueueParams<T>>::get().min_slashable_share *
Self::get_min_lowest_third_stake(from_era)
}

// Get the unbonding time, in eras, for quick unbond for an unbond request.
//
// We implement the calculation `unbonding_time_delta = new_unbonding_stake / max_unstake *
// upper bound period in blocks.
pub(crate) fn get_unbond_eras_delta(unbond_stake: BalanceOf<T>) -> EraIndex {
let upper_bound_usize: usize =
<UnbondingQueueParams<T>>::get().unbond_period_upper_bound.saturated_into();
let unbond_stake_usize: usize = unbond_stake.saturated_into();
let max_unstake_as_usize: usize =
Self::get_quick_unbond_max_unstake(CurrentEra::<T>::get().unwrap_or(0))
.saturated_into();

(unbond_stake_usize.saturating_div(max_unstake_as_usize) * upper_bound_usize)
.saturated_into()
}

/// Remove all associated data of a stash account from the staking system.
///
/// Assumes storage is upgraded before calling.
Expand Down
52 changes: 43 additions & 9 deletions substrate/frame/staking/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ mod impls;
pub use impls::*;

use crate::{
asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf,
DisablingStrategy, EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing,
LedgerIntegrityState, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota,
PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, UnappliedSlash,
asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, DisablingStrategy,
EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing, LedgerIntegrityState,
MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf,
RewardDestination, SessionInterface, StakingLedger, UnappliedSlash, UnbondingQueue,
UnlockChunk, ValidatorPrefs,
};

Expand All @@ -69,7 +69,7 @@ pub mod pallet {
use super::*;

/// The in-code storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(15);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(16);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -735,6 +735,17 @@ pub mod pallet {
#[pallet::storage]
pub(crate) type ChillThreshold<T: Config> = StorageValue<_, Percent, OptionQuery>;

/// The total amount of stake backed by the lowest third of validators for the last
/// upper bound eras. This is used to determine the maximum amount of stake that
/// can be unbonded for a period potentially lower than upper bound eras.
#[pallet::storage]
pub(crate) type EraLowestThirdTotalStake<T: Config> =
StorageMap<_, Twox64Concat, EraIndex, BalanceOf<T>>;

/// Parameters for the unbonding queue mechanism.
#[pallet::storage]
pub(crate) type UnbondingQueueParams<T: Config> = StorageValue<_, UnbondingQueue, ValueQuery>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub(crate) type UnbondingQueueParams<T: Config> = StorageValue<_, UnbondingQueue, ValueQuery>;
pub(crate) type UnbondingQueueParams<T: Config> = StorageValue<_, UnbondingQueue, OptionQuery>;

Maybe to not having a default and disable this feature when there is no parameter.
This requires a governance call to set the parameter to enable this feature, which I think should be the preferred way.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amended to OptionQuery and added an unbonding params entry to set_staking_configs.


#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
Expand Down Expand Up @@ -769,6 +780,7 @@ pub mod pallet {
if let Some(x) = self.max_nominator_count {
MaxNominatorsCount::<T>::put(x);
}
UnbondingQueueParams::<T>::set(Default::default());

for &(ref stash, _, balance, ref status) in &self.stakers {
crate::log!(
Expand Down Expand Up @@ -1141,10 +1153,32 @@ pub mod pallet {
// If a user runs into this error, they should chill first.
ensure!(ledger.active >= min_active_bond, Error::<T>::InsufficientBond);

let unbonding_queue_params = <UnbondingQueueParams<T>>::get();

// Note: in case there is no current era it is fine to bond one era more.
let era = Self::current_era()
.unwrap_or(0)
.defensive_saturating_add(T::BondingDuration::get());
let era = Self::current_era().unwrap_or(0);

// Calculate unbonding era based on unbonding queue mechanism.
let unbonding_eras_delta: EraIndex = Self::get_unbond_eras_delta(value);

let new_back_of_unbonding_queue_era: EraIndex = (era
.max(unbonding_queue_params.back_of_unbonding_queue_era) +
unbonding_eras_delta)
.min(unbonding_queue_params.unbond_period_upper_bound);

let unbonding_era: EraIndex = unbonding_queue_params.unbond_period_upper_bound.min(
new_back_of_unbonding_queue_era
.defensive_saturating_sub(era)
.max(unbonding_queue_params.unbond_period_lower_bound),
) + era;

// Update unbonding queue params with new `new_back_of_unbonding_queue_era`.
<UnbondingQueueParams<T>>::set(UnbondingQueue {
back_of_unbonding_queue_era: new_back_of_unbonding_queue_era,
..unbonding_queue_params
});

// Update chunks with the new unbonding era.
if let Some(chunk) = ledger.unlocking.last_mut().filter(|chunk| chunk.era == era) {
// To keep the chunk count down, we only keep one chunk per era. Since
// `unlocking` is a FiFo queue, if a chunk exists for `era` we know that it will
Expand All @@ -1153,7 +1187,7 @@ pub mod pallet {
} else {
ledger
.unlocking
.try_push(UnlockChunk { value, era })
.try_push(UnlockChunk { value, era: unbonding_era })
.map_err(|_| Error::<T>::NoMoreChunks)?;
};
// NOTE: ledger must be updated prior to calling `Self::weight_of`.
Expand Down
Loading