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

staking: Proportional ledger slashing #10982

Merged
merged 23 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
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 @@ -562,6 +563,7 @@ impl pallet_staking::Config for Runtime {
// Note that the aforementioned does not scale to a very large number of nominators.
type SortedListProvider = 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 @@ -183,6 +183,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 @@ -199,6 +200,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type SortedListProvider = pallet_staking::UseNominatorsMap<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 @@ -191,6 +191,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 @@ -207,6 +208,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type SortedListProvider = pallet_staking::UseNominatorsMap<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 @@ -158,6 +158,7 @@ impl onchain::Config for Test {
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 @@ -177,6 +178,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type SortedListProvider = pallet_staking::UseNominatorsMap<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 @@ -164,6 +164,7 @@ impl onchain::Config for Test {
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 @@ -183,6 +184,7 @@ impl pallet_staking::Config for Test {
type GenesisElectionProvider = Self::ElectionProvider;
type MaxUnlockingChunks = ConstU32<32>;
type SortedListProvider = pallet_staking::UseNominatorsMap<Self>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
7 changes: 4 additions & 3 deletions frame/staking/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ benchmarks! {
assert!(value * l.into() + origin_weight <= dest_weight);
let unlock_chunk = UnlockChunk::<BalanceOf<T>> {
value,
era: EraIndex::zero(),
unlock_era: EraIndex::zero(),
emostov marked this conversation as resolved.
Show resolved Hide resolved
};

let stash = scenario.origin_stash1.clone();
Expand Down Expand Up @@ -793,7 +793,7 @@ benchmarks! {
let mut staking_ledger = Ledger::<T>::get(controller.clone()).unwrap();
let unlock_chunk = UnlockChunk::<BalanceOf<T>> {
value: 1u32.into(),
era: EraIndex::zero(),
unlock_era: EraIndex::zero(),
};
for _ in 0 .. l {
staking_ledger.unlocking.try_push(unlock_chunk.clone()).unwrap();
Expand All @@ -806,7 +806,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
165 changes: 101 additions & 64 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 @@ -435,36 +434,35 @@ pub struct UnlockChunk<Balance: HasCompact> {
value: Balance,
/// Era number at which point it'll be unlocked.
#[codec(compact)]
era: EraIndex,
unlock_era: EraIndex,
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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 All @@ -482,7 +480,7 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
.unlocking
.into_iter()
.filter(|chunk| {
if chunk.era > current_era {
if chunk.unlock_era > current_era {
true
} else {
total = total.saturating_sub(chunk.value);
Expand All @@ -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>>) = {
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
if let Some(start_index) =
self.unlocking.iter().position(|c| c.unlock_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);
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
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 {
emostov marked this conversation as resolved.
Show resolved Hide resolved
emostov marked this conversation as resolved.
Show resolved Hide resolved
// 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.
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
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.unlock_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 @@ -235,6 +235,7 @@ const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] =
parameter_types! {
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
pub static MaxNominations: u32 = 16;
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
}

impl pallet_bags_list::Config for Test {
Expand All @@ -250,9 +251,21 @@ impl onchain::Config for Test {
type DataProvider = Staking;
}

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 @@ -273,6 +286,7 @@ impl crate::pallet::pallet::Config for Test {
// NOTE: consider a macro and use `UseNominatorsMap<Self>` as well.
type SortedListProvider = BagsList;
type MaxUnlockingChunks = ConstU32<32>;
type OnStakerSlash = OnStakerSlashMock<Test>;
type BenchmarkingConfig = TestBenchmarkingConfig;
type WeightInfo = ();
}
Expand Down
Loading