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

Commit

Permalink
staking: Proportional ledger slashing (paritytech#10982)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people committed Apr 21, 2022
1 parent 438b1f2 commit 4289b32
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 77 deletions.
2 changes: 2 additions & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig {
impl pallet_staking::Config for Runtime {
type MaxNominations = MaxNominations;
type Currency = Balances;
type CurrencyBalance = Balance;
type UnixTime = Timestamp;
type CurrencyToVote = U128CurrencyToVote;
type RewardRemainder = Treasury;
Expand All @@ -560,6 +561,7 @@ impl pallet_staking::Config for Runtime {
type GenesisElectionProvider = onchain::UnboundedExecution<OnChainSeqPhragmen>;
type VoterList = BagsList;
type MaxUnlockingChunks = ConstU32<32>;
type OnStakerSlash = ();
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
type BenchmarkingConfig = StakingBenchmarkingConfig;
}
Expand Down
2 changes: 2 additions & 0 deletions frame/babe/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ impl pallet_staking::Config for Test {
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type Event = Event;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type Slash = ();
type Reward = ();
type SessionsPerEra = SessionsPerEra;
Expand All @@ -202,6 +203,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
2 changes: 2 additions & 0 deletions frame/grandpa/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ impl pallet_staking::Config for Test {
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type Event = Event;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type Slash = ();
type Reward = ();
type SessionsPerEra = SessionsPerEra;
Expand All @@ -210,6 +211,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
2 changes: 2 additions & 0 deletions frame/offences/benchmarking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ impl onchain::Config for OnChainSeqPhragmen {
impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = pallet_timestamp::Pallet<Self>;
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type RewardRemainder = ();
Expand All @@ -180,6 +181,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
2 changes: 2 additions & 0 deletions frame/session/benchmarking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ impl onchain::Config for OnChainSeqPhragmen {
impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = pallet_timestamp::Pallet<Self>;
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type RewardRemainder = ();
Expand All @@ -186,6 +187,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type MaxUnlockingChunks = ConstU32<32>;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
3 changes: 2 additions & 1 deletion frame/staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,8 @@ benchmarks! {
&stash,
slash_amount,
&mut BalanceOf::<T>::zero(),
&mut NegativeImbalanceOf::<T>::zero()
&mut NegativeImbalanceOf::<T>::zero(),
EraIndex::zero()
);
} verify {
let balance_after = T::Currency::free_balance(&stash);
Expand Down
161 changes: 99 additions & 62 deletions frame/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,15 +302,15 @@ 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,
};
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},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand All @@ -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;

let slash_out_of =
|total_remaining: &mut Balance, target: &mut Balance, value: &mut Balance| {
let mut slash_from_target = (*value).min(*target);

if !slash_from_target.is_zero() {
*target -= slash_from_target;

// 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());
}

*total_remaining = total_remaining.saturating_sub(slash_from_target);
*value -= slash_from_target;
}
};

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();
/// # 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 _;

if slash_amount.is_zero() {
return Zero::zero()
}

// Kill all drained chunks.
let _ = self.unlocking.drain(..i);
let mut remaining_slash = slash_amount;
let pre_slash_total = self.total;

let era_after_slash = slash_era + 1;
let chunk_unlock_era_after_slash = era_after_slash + T::BondingDuration::get();

// 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)
}

pre_total.saturating_sub(*total)
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
}
} 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)
}
}

Expand Down
14 changes: 14 additions & 0 deletions frame/staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ parameter_types! {
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
pub static MaxNominations: u32 = 16;
pub static RewardOnUnbalanceWasCalled: bool = false;
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
}

impl pallet_bags_list::Config for Test {
Expand All @@ -263,9 +264,21 @@ impl OnUnbalanced<PositiveImbalanceOf<Test>> for MockReward {
}
}

pub struct OnStakerSlashMock<T: Config>(core::marker::PhantomData<T>);
impl<T: Config> sp_staking::OnStakerSlash<AccountId, Balance> for OnStakerSlashMock<T> {
fn on_slash(
_pool_account: &AccountId,
slashed_bonded: Balance,
slashed_chunks: &BTreeMap<EraIndex, Balance>,
) {
LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
}
}

impl crate::pallet::pallet::Config for Test {
type MaxNominations = MaxNominations;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type UnixTime = Timestamp;
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type RewardRemainder = RewardRemainderMock;
Expand All @@ -286,6 +299,7 @@ impl crate::pallet::pallet::Config for Test {
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = BagsList;
type MaxUnlockingChunks = ConstU32<32>;
type OnStakerSlash = OnStakerSlashMock<Test>;
type BenchmarkingConfig = TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
9 changes: 3 additions & 6 deletions frame/staking/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,7 @@ impl<T: Config> Pallet<T> {
/// Update the ledger for a controller.
///
/// This will also update the stash lock.
pub(crate) fn update_ledger(
controller: &T::AccountId,
ledger: &StakingLedger<T::AccountId, BalanceOf<T>>,
) {
pub(crate) fn update_ledger(controller: &T::AccountId, ledger: &StakingLedger<T>) {
T::Currency::set_lock(STAKING_ID, &ledger.stash, ledger.total, WithdrawReasons::all());
<Ledger<T>>::insert(controller, ledger);
}
Expand Down Expand Up @@ -606,7 +603,7 @@ impl<T: Config> Pallet<T> {
for era in (*earliest)..keep_from {
let era_slashes = <Self as Store>::UnappliedSlashes::take(&era);
for slash in era_slashes {
slashing::apply_slash::<T>(slash);
slashing::apply_slash::<T>(slash, era);
}
}

Expand Down Expand Up @@ -1248,7 +1245,7 @@ where
unapplied.reporters = details.reporters.clone();
if slash_defer_duration == 0 {
// Apply right away.
slashing::apply_slash::<T>(unapplied);
slashing::apply_slash::<T>(unapplied, slash_era);
{
let slash_cost = (6, 5);
let reward_cost = (2, 2);
Expand Down
Loading

0 comments on commit 4289b32

Please sign in to comment.