mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-06 03:17:25 +00:00
staking: Proportional ledger slashing (#10982)
* staking: Proportional ledger slashing * Some comment cleanup * Update frame/staking/src/pallet/mod.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Fix benchmarks * FMT * Try fill in all staking configs * round of feedback and imp from kian * demonstrate per_thing usage * Update some tests * FMT * Test that era offset works correctly * Update mocks * Remove unnescary docs * Remove unlock_era * Update frame/staking/src/lib.rs * Adjust tests to account for only remove when < ED * Remove stale TODOs * Remove dupe test Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: kianenigma <kian@parity.io>
This commit is contained in:
@@ -302,7 +302,7 @@ mod pallet;
|
||||
use codec::{Decode, Encode, HasCompact};
|
||||
use frame_support::{
|
||||
parameter_types,
|
||||
traits::{Currency, Get},
|
||||
traits::{Currency, Defensive, Get},
|
||||
weights::Weight,
|
||||
BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
@@ -310,7 +310,7 @@ use scale_info::TypeInfo;
|
||||
use sp_runtime::{
|
||||
curve::PiecewiseLinear,
|
||||
traits::{AtLeast32BitUnsigned, Convert, Saturating, Zero},
|
||||
Perbill, RuntimeDebug,
|
||||
Perbill, Perquintill, RuntimeDebug,
|
||||
};
|
||||
use sp_staking::{
|
||||
offence::{Offence, OffenceError, ReportOffence},
|
||||
@@ -338,8 +338,7 @@ macro_rules! log {
|
||||
pub type RewardPoint = u32;
|
||||
|
||||
/// The balance type of this pallet.
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
|
||||
|
||||
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
@@ -440,31 +439,30 @@ pub struct UnlockChunk<Balance: HasCompact> {
|
||||
|
||||
/// The ledger of a (bonded) stash.
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub struct StakingLedger<AccountId, Balance: HasCompact> {
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct StakingLedger<T: Config> {
|
||||
/// The stash account whose balance is actually locked and at stake.
|
||||
pub stash: AccountId,
|
||||
pub stash: T::AccountId,
|
||||
/// The total amount of the stash's balance that we are currently accounting for.
|
||||
/// It's just `active` plus all the `unlocking` balances.
|
||||
#[codec(compact)]
|
||||
pub total: Balance,
|
||||
pub total: BalanceOf<T>,
|
||||
/// The total amount of the stash's balance that will be at stake in any forthcoming
|
||||
/// rounds.
|
||||
#[codec(compact)]
|
||||
pub active: Balance,
|
||||
pub active: BalanceOf<T>,
|
||||
/// 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<Balance>, MaxUnlockingChunks>,
|
||||
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, MaxUnlockingChunks>,
|
||||
/// List of eras for which the stakers behind a validator have claimed rewards. Only updated
|
||||
/// for validators.
|
||||
pub claimed_rewards: Vec<EraIndex>,
|
||||
}
|
||||
|
||||
impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned + Zero>
|
||||
StakingLedger<AccountId, Balance>
|
||||
{
|
||||
impl<T: Config> StakingLedger<T> {
|
||||
/// Initializes the default object using the given `validator`.
|
||||
pub fn default_from(stash: AccountId) -> Self {
|
||||
pub fn default_from(stash: T::AccountId) -> Self {
|
||||
Self {
|
||||
stash,
|
||||
total: Zero::zero(),
|
||||
@@ -507,8 +505,8 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
|
||||
/// Re-bond funds that were scheduled for unlocking.
|
||||
///
|
||||
/// Returns the updated ledger, and the amount actually rebonded.
|
||||
fn rebond(mut self, value: Balance) -> (Self, Balance) {
|
||||
let mut unlocking_balance: Balance = Zero::zero();
|
||||
fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
|
||||
let mut unlocking_balance = BalanceOf::<T>::zero();
|
||||
|
||||
while let Some(last) = self.unlocking.last_mut() {
|
||||
if unlocking_balance + last.value <= value {
|
||||
@@ -530,57 +528,96 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
|
||||
|
||||
(self, unlocking_balance)
|
||||
}
|
||||
}
|
||||
|
||||
impl<AccountId, Balance> StakingLedger<AccountId, Balance>
|
||||
where
|
||||
Balance: AtLeast32BitUnsigned + Saturating + Copy,
|
||||
{
|
||||
/// Slash the validator for a given amount of balance. This can grow the value
|
||||
/// of the slash in the case that the validator has less than `minimum_balance`
|
||||
/// active funds. Returns the amount of funds actually slashed.
|
||||
/// Slash the staker for a given amount of balance. This can grow the value of the slash in the
|
||||
/// case that either the active bonded or some unlocking chunks become dust after slashing.
|
||||
/// Returns the amount of funds actually slashed.
|
||||
///
|
||||
/// Slashes from `active` funds first, and then `unlocking`, starting with the
|
||||
/// chunks that are closest to unlocking.
|
||||
fn slash(&mut self, mut value: Balance, minimum_balance: Balance) -> Balance {
|
||||
let pre_total = self.total;
|
||||
let total = &mut self.total;
|
||||
let active = &mut self.active;
|
||||
/// # Note
|
||||
///
|
||||
/// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash
|
||||
/// was applied.
|
||||
fn slash(
|
||||
&mut self,
|
||||
slash_amount: BalanceOf<T>,
|
||||
minimum_balance: BalanceOf<T>,
|
||||
slash_era: EraIndex,
|
||||
) -> BalanceOf<T> {
|
||||
use sp_staking::OnStakerSlash as _;
|
||||
|
||||
let slash_out_of =
|
||||
|total_remaining: &mut Balance, target: &mut Balance, value: &mut Balance| {
|
||||
let mut slash_from_target = (*value).min(*target);
|
||||
if slash_amount.is_zero() {
|
||||
return Zero::zero()
|
||||
}
|
||||
|
||||
if !slash_from_target.is_zero() {
|
||||
*target -= slash_from_target;
|
||||
let mut remaining_slash = slash_amount;
|
||||
let pre_slash_total = self.total;
|
||||
|
||||
// Don't leave a dust balance in the staking system.
|
||||
if *target <= minimum_balance {
|
||||
slash_from_target += *target;
|
||||
*value += sp_std::mem::replace(target, Zero::zero());
|
||||
}
|
||||
let era_after_slash = slash_era + 1;
|
||||
let chunk_unlock_era_after_slash = era_after_slash + T::BondingDuration::get();
|
||||
|
||||
*total_remaining = total_remaining.saturating_sub(slash_from_target);
|
||||
*value -= slash_from_target;
|
||||
// Calculate the total balance of active funds and unlocking funds in the affected range.
|
||||
let (affected_balance, slash_chunks_priority): (_, Box<dyn Iterator<Item = usize>>) = {
|
||||
if let Some(start_index) =
|
||||
self.unlocking.iter().position(|c| c.era >= chunk_unlock_era_after_slash)
|
||||
{
|
||||
// The indices of the first chunk after the slash up through the most recent chunk.
|
||||
// (The most recent chunk is at greatest from this era)
|
||||
let affected_indices = start_index..self.unlocking.len();
|
||||
let unbonding_affected_balance =
|
||||
affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
|
||||
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
|
||||
sum.saturating_add(chunk.value)
|
||||
} else {
|
||||
sum
|
||||
}
|
||||
});
|
||||
(
|
||||
self.active.saturating_add(unbonding_affected_balance),
|
||||
Box::new(affected_indices.chain((0..start_index).rev())),
|
||||
)
|
||||
} else {
|
||||
(self.active, Box::new((0..self.unlocking.len()).rev()))
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to update `target` and the ledgers total after accounting for slashing `target`.
|
||||
let ratio = Perquintill::from_rational(slash_amount, affected_balance);
|
||||
let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
|
||||
let mut slash_from_target =
|
||||
if slash_amount < affected_balance { ratio * (*target) } else { *slash_remaining }
|
||||
.min(*target);
|
||||
|
||||
// slash out from *target exactly `slash_from_target`.
|
||||
*target = *target - slash_from_target;
|
||||
if *target < minimum_balance {
|
||||
// Slash the rest of the target if its dust
|
||||
slash_from_target =
|
||||
sp_std::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
|
||||
}
|
||||
|
||||
self.total = self.total.saturating_sub(slash_from_target);
|
||||
*slash_remaining = slash_remaining.saturating_sub(slash_from_target);
|
||||
};
|
||||
|
||||
// If this is *not* a proportional slash, the active will always wiped to 0.
|
||||
slash_out_of(&mut self.active, &mut remaining_slash);
|
||||
|
||||
let mut slashed_unlocking = BTreeMap::<_, _>::new();
|
||||
for i in slash_chunks_priority {
|
||||
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
|
||||
slash_out_of(&mut chunk.value, &mut remaining_slash);
|
||||
// write the new slashed value of this chunk to the map.
|
||||
slashed_unlocking.insert(chunk.era, chunk.value);
|
||||
if remaining_slash.is_zero() {
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
slash_out_of(total, active, &mut value);
|
||||
|
||||
let i = self
|
||||
.unlocking
|
||||
.iter_mut()
|
||||
.map(|chunk| {
|
||||
slash_out_of(total, &mut chunk.value, &mut value);
|
||||
chunk.value
|
||||
})
|
||||
.take_while(|value| value.is_zero()) // Take all fully-consumed chunks out.
|
||||
.count();
|
||||
|
||||
// Kill all drained chunks.
|
||||
let _ = self.unlocking.drain(..i);
|
||||
|
||||
pre_total.saturating_sub(*total)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.unlocking.retain(|c| !c.value.is_zero());
|
||||
T::OnStakerSlash::on_slash(&self.stash, self.active, &slashed_unlocking);
|
||||
pre_slash_total.saturating_sub(self.total)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user