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

Commit 4289b32

Browse files
emostovkianenigma
andauthored
staking: Proportional ledger slashing (#10982)
* staking: Proportional ledger slashing * Some comment cleanup * Update frame/staking/src/pallet/mod.rs Co-authored-by: Kian Paimani <[email protected]> * 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 <[email protected]> Co-authored-by: kianenigma <[email protected]>
1 parent 438b1f2 commit 4289b32

File tree

13 files changed

+402
-77
lines changed

13 files changed

+402
-77
lines changed

bin/node/runtime/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig {
537537
impl pallet_staking::Config for Runtime {
538538
type MaxNominations = MaxNominations;
539539
type Currency = Balances;
540+
type CurrencyBalance = Balance;
540541
type UnixTime = Timestamp;
541542
type CurrencyToVote = U128CurrencyToVote;
542543
type RewardRemainder = Treasury;
@@ -560,6 +561,7 @@ impl pallet_staking::Config for Runtime {
560561
type GenesisElectionProvider = onchain::UnboundedExecution<OnChainSeqPhragmen>;
561562
type VoterList = BagsList;
562563
type MaxUnlockingChunks = ConstU32<32>;
564+
type OnStakerSlash = ();
563565
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
564566
type BenchmarkingConfig = StakingBenchmarkingConfig;
565567
}

frame/babe/src/mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ impl pallet_staking::Config for Test {
186186
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
187187
type Event = Event;
188188
type Currency = Balances;
189+
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
189190
type Slash = ();
190191
type Reward = ();
191192
type SessionsPerEra = SessionsPerEra;
@@ -202,6 +203,7 @@ impl pallet_staking::Config for Test {
202203
type GenesisElectionProvider = Self::ElectionProvider;
203204
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
204205
type MaxUnlockingChunks = ConstU32<32>;
206+
type OnStakerSlash = ();
205207
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
206208
type WeightInfo = ();
207209
}

frame/grandpa/src/mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ impl pallet_staking::Config for Test {
194194
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
195195
type Event = Event;
196196
type Currency = Balances;
197+
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
197198
type Slash = ();
198199
type Reward = ();
199200
type SessionsPerEra = SessionsPerEra;
@@ -210,6 +211,7 @@ impl pallet_staking::Config for Test {
210211
type GenesisElectionProvider = Self::ElectionProvider;
211212
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
212213
type MaxUnlockingChunks = ConstU32<32>;
214+
type OnStakerSlash = ();
213215
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
214216
type WeightInfo = ();
215217
}

frame/offences/benchmarking/src/mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ impl onchain::Config for OnChainSeqPhragmen {
161161
impl pallet_staking::Config for Test {
162162
type MaxNominations = ConstU32<16>;
163163
type Currency = Balances;
164+
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
164165
type UnixTime = pallet_timestamp::Pallet<Self>;
165166
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
166167
type RewardRemainder = ();
@@ -180,6 +181,7 @@ impl pallet_staking::Config for Test {
180181
type GenesisElectionProvider = Self::ElectionProvider;
181182
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
182183
type MaxUnlockingChunks = ConstU32<32>;
184+
type OnStakerSlash = ();
183185
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
184186
type WeightInfo = ();
185187
}

frame/session/benchmarking/src/mock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ impl onchain::Config for OnChainSeqPhragmen {
167167
impl pallet_staking::Config for Test {
168168
type MaxNominations = ConstU32<16>;
169169
type Currency = Balances;
170+
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
170171
type UnixTime = pallet_timestamp::Pallet<Self>;
171172
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
172173
type RewardRemainder = ();
@@ -186,6 +187,7 @@ impl pallet_staking::Config for Test {
186187
type GenesisElectionProvider = Self::ElectionProvider;
187188
type MaxUnlockingChunks = ConstU32<32>;
188189
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
190+
type OnStakerSlash = ();
189191
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
190192
type WeightInfo = ();
191193
}

frame/staking/src/benchmarking.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,8 @@ benchmarks! {
802802
&stash,
803803
slash_amount,
804804
&mut BalanceOf::<T>::zero(),
805-
&mut NegativeImbalanceOf::<T>::zero()
805+
&mut NegativeImbalanceOf::<T>::zero(),
806+
EraIndex::zero()
806807
);
807808
} verify {
808809
let balance_after = T::Currency::free_balance(&stash);

frame/staking/src/lib.rs

Lines changed: 99 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -302,15 +302,15 @@ mod pallet;
302302
use codec::{Decode, Encode, HasCompact};
303303
use frame_support::{
304304
parameter_types,
305-
traits::{Currency, Get},
305+
traits::{Currency, Defensive, Get},
306306
weights::Weight,
307307
BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
308308
};
309309
use scale_info::TypeInfo;
310310
use sp_runtime::{
311311
curve::PiecewiseLinear,
312312
traits::{AtLeast32BitUnsigned, Convert, Saturating, Zero},
313-
Perbill, RuntimeDebug,
313+
Perbill, Perquintill, RuntimeDebug,
314314
};
315315
use sp_staking::{
316316
offence::{Offence, OffenceError, ReportOffence},
@@ -338,8 +338,7 @@ macro_rules! log {
338338
pub type RewardPoint = u32;
339339

340340
/// The balance type of this pallet.
341-
pub type BalanceOf<T> =
342-
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
341+
pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
343342

344343
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
345344
<T as frame_system::Config>::AccountId,
@@ -440,31 +439,30 @@ pub struct UnlockChunk<Balance: HasCompact> {
440439

441440
/// The ledger of a (bonded) stash.
442441
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
443-
pub struct StakingLedger<AccountId, Balance: HasCompact> {
442+
#[scale_info(skip_type_params(T))]
443+
pub struct StakingLedger<T: Config> {
444444
/// The stash account whose balance is actually locked and at stake.
445-
pub stash: AccountId,
445+
pub stash: T::AccountId,
446446
/// The total amount of the stash's balance that we are currently accounting for.
447447
/// It's just `active` plus all the `unlocking` balances.
448448
#[codec(compact)]
449-
pub total: Balance,
449+
pub total: BalanceOf<T>,
450450
/// The total amount of the stash's balance that will be at stake in any forthcoming
451451
/// rounds.
452452
#[codec(compact)]
453-
pub active: Balance,
453+
pub active: BalanceOf<T>,
454454
/// Any balance that is becoming free, which may eventually be transferred out of the stash
455455
/// (assuming it doesn't get slashed first). It is assumed that this will be treated as a first
456456
/// in, first out queue where the new (higher value) eras get pushed on the back.
457-
pub unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks>,
457+
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, MaxUnlockingChunks>,
458458
/// List of eras for which the stakers behind a validator have claimed rewards. Only updated
459459
/// for validators.
460460
pub claimed_rewards: Vec<EraIndex>,
461461
}
462462

463-
impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned + Zero>
464-
StakingLedger<AccountId, Balance>
465-
{
463+
impl<T: Config> StakingLedger<T> {
466464
/// Initializes the default object using the given `validator`.
467-
pub fn default_from(stash: AccountId) -> Self {
465+
pub fn default_from(stash: T::AccountId) -> Self {
468466
Self {
469467
stash,
470468
total: Zero::zero(),
@@ -507,8 +505,8 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
507505
/// Re-bond funds that were scheduled for unlocking.
508506
///
509507
/// Returns the updated ledger, and the amount actually rebonded.
510-
fn rebond(mut self, value: Balance) -> (Self, Balance) {
511-
let mut unlocking_balance: Balance = Zero::zero();
508+
fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
509+
let mut unlocking_balance = BalanceOf::<T>::zero();
512510

513511
while let Some(last) = self.unlocking.last_mut() {
514512
if unlocking_balance + last.value <= value {
@@ -530,57 +528,96 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
530528

531529
(self, unlocking_balance)
532530
}
533-
}
534531

535-
impl<AccountId, Balance> StakingLedger<AccountId, Balance>
536-
where
537-
Balance: AtLeast32BitUnsigned + Saturating + Copy,
538-
{
539-
/// Slash the validator for a given amount of balance. This can grow the value
540-
/// of the slash in the case that the validator has less than `minimum_balance`
541-
/// active funds. Returns the amount of funds actually slashed.
532+
/// Slash the staker for a given amount of balance. This can grow the value of the slash in the
533+
/// case that either the active bonded or some unlocking chunks become dust after slashing.
534+
/// Returns the amount of funds actually slashed.
542535
///
543-
/// Slashes from `active` funds first, and then `unlocking`, starting with the
544-
/// chunks that are closest to unlocking.
545-
fn slash(&mut self, mut value: Balance, minimum_balance: Balance) -> Balance {
546-
let pre_total = self.total;
547-
let total = &mut self.total;
548-
let active = &mut self.active;
549-
550-
let slash_out_of =
551-
|total_remaining: &mut Balance, target: &mut Balance, value: &mut Balance| {
552-
let mut slash_from_target = (*value).min(*target);
553-
554-
if !slash_from_target.is_zero() {
555-
*target -= slash_from_target;
556-
557-
// Don't leave a dust balance in the staking system.
558-
if *target <= minimum_balance {
559-
slash_from_target += *target;
560-
*value += sp_std::mem::replace(target, Zero::zero());
561-
}
562-
563-
*total_remaining = total_remaining.saturating_sub(slash_from_target);
564-
*value -= slash_from_target;
565-
}
566-
};
567-
568-
slash_out_of(total, active, &mut value);
569-
570-
let i = self
571-
.unlocking
572-
.iter_mut()
573-
.map(|chunk| {
574-
slash_out_of(total, &mut chunk.value, &mut value);
575-
chunk.value
576-
})
577-
.take_while(|value| value.is_zero()) // Take all fully-consumed chunks out.
578-
.count();
536+
/// # Note
537+
///
538+
/// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash
539+
/// was applied.
540+
fn slash(
541+
&mut self,
542+
slash_amount: BalanceOf<T>,
543+
minimum_balance: BalanceOf<T>,
544+
slash_era: EraIndex,
545+
) -> BalanceOf<T> {
546+
use sp_staking::OnStakerSlash as _;
547+
548+
if slash_amount.is_zero() {
549+
return Zero::zero()
550+
}
579551

580-
// Kill all drained chunks.
581-
let _ = self.unlocking.drain(..i);
552+
let mut remaining_slash = slash_amount;
553+
let pre_slash_total = self.total;
554+
555+
let era_after_slash = slash_era + 1;
556+
let chunk_unlock_era_after_slash = era_after_slash + T::BondingDuration::get();
557+
558+
// Calculate the total balance of active funds and unlocking funds in the affected range.
559+
let (affected_balance, slash_chunks_priority): (_, Box<dyn Iterator<Item = usize>>) = {
560+
if let Some(start_index) =
561+
self.unlocking.iter().position(|c| c.era >= chunk_unlock_era_after_slash)
562+
{
563+
// The indices of the first chunk after the slash up through the most recent chunk.
564+
// (The most recent chunk is at greatest from this era)
565+
let affected_indices = start_index..self.unlocking.len();
566+
let unbonding_affected_balance =
567+
affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
568+
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
569+
sum.saturating_add(chunk.value)
570+
} else {
571+
sum
572+
}
573+
});
574+
(
575+
self.active.saturating_add(unbonding_affected_balance),
576+
Box::new(affected_indices.chain((0..start_index).rev())),
577+
)
578+
} else {
579+
(self.active, Box::new((0..self.unlocking.len()).rev()))
580+
}
581+
};
582+
583+
// Helper to update `target` and the ledgers total after accounting for slashing `target`.
584+
let ratio = Perquintill::from_rational(slash_amount, affected_balance);
585+
let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
586+
let mut slash_from_target =
587+
if slash_amount < affected_balance { ratio * (*target) } else { *slash_remaining }
588+
.min(*target);
589+
590+
// slash out from *target exactly `slash_from_target`.
591+
*target = *target - slash_from_target;
592+
if *target < minimum_balance {
593+
// Slash the rest of the target if its dust
594+
slash_from_target =
595+
sp_std::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
596+
}
582597

583-
pre_total.saturating_sub(*total)
598+
self.total = self.total.saturating_sub(slash_from_target);
599+
*slash_remaining = slash_remaining.saturating_sub(slash_from_target);
600+
};
601+
602+
// If this is *not* a proportional slash, the active will always wiped to 0.
603+
slash_out_of(&mut self.active, &mut remaining_slash);
604+
605+
let mut slashed_unlocking = BTreeMap::<_, _>::new();
606+
for i in slash_chunks_priority {
607+
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
608+
slash_out_of(&mut chunk.value, &mut remaining_slash);
609+
// write the new slashed value of this chunk to the map.
610+
slashed_unlocking.insert(chunk.era, chunk.value);
611+
if remaining_slash.is_zero() {
612+
break
613+
}
614+
} else {
615+
break
616+
}
617+
}
618+
self.unlocking.retain(|c| !c.value.is_zero());
619+
T::OnStakerSlash::on_slash(&self.stash, self.active, &slashed_unlocking);
620+
pre_slash_total.saturating_sub(self.total)
584621
}
585622
}
586623

frame/staking/src/mock.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ parameter_types! {
238238
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
239239
pub static MaxNominations: u32 = 16;
240240
pub static RewardOnUnbalanceWasCalled: bool = false;
241+
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
241242
}
242243

243244
impl pallet_bags_list::Config for Test {
@@ -263,9 +264,21 @@ impl OnUnbalanced<PositiveImbalanceOf<Test>> for MockReward {
263264
}
264265
}
265266

267+
pub struct OnStakerSlashMock<T: Config>(core::marker::PhantomData<T>);
268+
impl<T: Config> sp_staking::OnStakerSlash<AccountId, Balance> for OnStakerSlashMock<T> {
269+
fn on_slash(
270+
_pool_account: &AccountId,
271+
slashed_bonded: Balance,
272+
slashed_chunks: &BTreeMap<EraIndex, Balance>,
273+
) {
274+
LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
275+
}
276+
}
277+
266278
impl crate::pallet::pallet::Config for Test {
267279
type MaxNominations = MaxNominations;
268280
type Currency = Balances;
281+
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
269282
type UnixTime = Timestamp;
270283
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
271284
type RewardRemainder = RewardRemainderMock;
@@ -286,6 +299,7 @@ impl crate::pallet::pallet::Config for Test {
286299
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
287300
type VoterList = BagsList;
288301
type MaxUnlockingChunks = ConstU32<32>;
302+
type OnStakerSlash = OnStakerSlashMock<Test>;
289303
type BenchmarkingConfig = TestBenchmarkingConfig;
290304
type WeightInfo = ();
291305
}

frame/staking/src/pallet/impls.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,7 @@ impl<T: Config> Pallet<T> {
213213
/// Update the ledger for a controller.
214214
///
215215
/// This will also update the stash lock.
216-
pub(crate) fn update_ledger(
217-
controller: &T::AccountId,
218-
ledger: &StakingLedger<T::AccountId, BalanceOf<T>>,
219-
) {
216+
pub(crate) fn update_ledger(controller: &T::AccountId, ledger: &StakingLedger<T>) {
220217
T::Currency::set_lock(STAKING_ID, &ledger.stash, ledger.total, WithdrawReasons::all());
221218
<Ledger<T>>::insert(controller, ledger);
222219
}
@@ -606,7 +603,7 @@ impl<T: Config> Pallet<T> {
606603
for era in (*earliest)..keep_from {
607604
let era_slashes = <Self as Store>::UnappliedSlashes::take(&era);
608605
for slash in era_slashes {
609-
slashing::apply_slash::<T>(slash);
606+
slashing::apply_slash::<T>(slash, era);
610607
}
611608
}
612609

@@ -1248,7 +1245,7 @@ where
12481245
unapplied.reporters = details.reporters.clone();
12491246
if slash_defer_duration == 0 {
12501247
// Apply right away.
1251-
slashing::apply_slash::<T>(unapplied);
1248+
slashing::apply_slash::<T>(unapplied, slash_era);
12521249
{
12531250
let slash_cost = (6, 5);
12541251
let reward_cost = (2, 2);

0 commit comments

Comments
 (0)