feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,163 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Contains all the interactions with [`Config::Currency`] to manipulate the underlying staking
//! asset.
use crate::{BalanceOf, Config, HoldReason, NegativeImbalanceOf, PositiveImbalanceOf};
use pezframe_support::traits::{
fungible::{
hold::{Balanced as FunHoldBalanced, Inspect as FunHoldInspect, Mutate as FunHoldMutate},
Balanced, Inspect as FunInspect,
},
tokens::{Fortitude, Precision, Preservation},
};
use pezsp_runtime::{DispatchResult, Saturating};
/// Existential deposit for the chain.
pub fn existential_deposit<T: Config>() -> BalanceOf<T> {
T::Currency::minimum_balance()
}
/// Total issuance of the chain.
pub fn total_issuance<T: Config>() -> BalanceOf<T> {
T::Currency::total_issuance()
}
/// Total balance of `who`. Includes both free and staked.
pub fn total_balance<T: Config>(who: &T::AccountId) -> BalanceOf<T> {
T::Currency::total_balance(who)
}
/// Stakeable balance of `who`.
///
/// This includes balance free to stake along with any balance that is already staked.
pub fn stakeable_balance<T: Config>(who: &T::AccountId) -> BalanceOf<T> {
free_to_stake::<T>(who).saturating_add(staked::<T>(who))
}
/// Balance of `who` that is currently at stake.
///
/// The staked amount is on hold and cannot be transferred out of `who`s account.
pub fn staked<T: Config>(who: &T::AccountId) -> BalanceOf<T> {
T::Currency::balance_on_hold(&HoldReason::Staking.into(), who)
}
/// Balance of who that can be staked additionally.
///
/// Does not include the current stake.
pub fn free_to_stake<T: Config>(who: &T::AccountId) -> BalanceOf<T> {
// since we want to be able to use frozen funds for staking, we force the reduction.
T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Force)
}
/// Update `amount` at stake for `who`.
///
/// Overwrites the existing stake amount. If passed amount is lower than the existing stake, the
/// difference is unlocked.
pub fn update_stake<T: Config>(who: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {
T::Currency::set_on_hold(&HoldReason::Staking.into(), who, amount)
}
/// Release all staked amount to `who`.
///
/// Fails if there are consumers left on `who` that restricts it from being reaped.
pub fn kill_stake<T: Config>(who: &T::AccountId) -> DispatchResult {
T::Currency::release_all(&HoldReason::Staking.into(), who, Precision::BestEffort).map(|_| ())
}
/// Slash the value from `who`.
///
/// A negative imbalance is returned which can be resolved to deposit the slashed value.
pub fn slash<T: Config>(
who: &T::AccountId,
value: BalanceOf<T>,
) -> (NegativeImbalanceOf<T>, BalanceOf<T>) {
T::Currency::slash(&HoldReason::Staking.into(), who, value)
}
/// Mint `value` into an existing account `who`.
///
/// This does not increase the total issuance.
pub fn mint_into_existing<T: Config>(
who: &T::AccountId,
value: BalanceOf<T>,
) -> Option<PositiveImbalanceOf<T>> {
// since the account already exists, we mint exact value even if value is below ED.
T::Currency::deposit(who, value, Precision::Exact).ok()
}
/// Mint `value` and create account for `who` if it does not exist.
///
/// If value is below existential deposit, the account is not created.
///
/// Note: This does not increase the total issuance.
pub fn mint_creating<T: Config>(who: &T::AccountId, value: BalanceOf<T>) -> PositiveImbalanceOf<T> {
T::Currency::deposit(who, value, Precision::BestEffort).unwrap_or_default()
}
/// Deposit newly issued or slashed `value` into `who`.
pub fn deposit_slashed<T: Config>(who: &T::AccountId, value: NegativeImbalanceOf<T>) {
let _ = T::Currency::resolve(who, value);
}
/// Issue `value` increasing total issuance.
///
/// Creates a negative imbalance.
pub fn issue<T: Config>(value: BalanceOf<T>) -> NegativeImbalanceOf<T> {
T::Currency::issue(value)
}
/// Burn the amount from the total issuance.
#[cfg(feature = "runtime-benchmarks")]
pub fn burn<T: Config>(amount: BalanceOf<T>) -> PositiveImbalanceOf<T> {
T::Currency::rescind(amount)
}
/// Set balance that can be staked for `who`.
///
/// If `Value` is lower than the current staked balance, the difference is unlocked.
///
/// Should only be used with test.
#[cfg(any(test, feature = "runtime-benchmarks"))]
pub fn set_stakeable_balance<T: Config>(who: &T::AccountId, value: BalanceOf<T>) {
use pezframe_support::traits::fungible::Mutate;
// minimum free balance (non-staked) required to keep the account alive.
let ed = existential_deposit::<T>();
// currently on stake
let staked_balance = staked::<T>(who);
// if new value is greater than staked balance, mint some free balance.
if value > staked_balance {
let _ = T::Currency::set_balance(who, value - staked_balance + ed);
} else {
// else reduce the staked balance.
update_stake::<T>(who, value).expect("can remove from what is staked");
// burn all free, only leaving ED.
let _ = T::Currency::set_balance(who, ed);
}
// ensure new stakeable balance same as desired `value`.
assert_eq!(stakeable_balance::<T>(who), value);
}
/// Return the amount staked and available to stake in one tuple.
#[cfg(test)]
pub fn staked_and_not<T: Config>(who: &T::AccountId) -> (BalanceOf<T>, BalanceOf<T>) {
(staked::<T>(who), free_to_stake::<T>(who))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,259 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! ## A static size tracker for the election snapshot data.
//!
//! ### Overview
//!
//! The goal of the size tracker is to provide a static, no-allocation byte tracker to be
//! used by the election data provider when preparing the results of
//! [`ElectionDataProvider::electing_voters`]. The [`StaticTracker`] implementation uses
//! [`codec::Encode::size_hint`] to estimate the SCALE encoded size of the snapshot voters struct
//! as it is being constructed without requiring extra stack allocations.
//!
//! The [`StaticTracker::try_register_voter`] is called to update the static tracker internal
//! state, if It will return an error if the resulting SCALE encoded size (in bytes) is larger than
//! the provided `DataProviderBounds`.
//!
//! ### Example
//!
//! ```ignore
//! use pezpallet_staking_async::election_size_tracker::*;
//!
//! // instantiates a new tracker.
//! let mut size_tracker = StaticTracker::<Staking>::default();
//!
//! let voter_bounds = ElectionBoundsBuilder::default().voter_size(1_00.into()).build().voters;
//!
//! let mut sorted_voters = T::VoterList.iter();
//! let mut selected_voters = vec![];
//!
//! // fit as many voters in the vec as the bounds permit.
//! for v in sorted_voters {
//! let voter = (v, weight_of(&v), targets_of(&v));
//! if size_tracker.try_register_voter(&voter, &voter_bounds).is_err() {
//! // voter bounds size exhausted
//! break;
//! }
//! selected_voters.push(voter);
//! }
//!
//! // The SCALE encoded size in bytes of `selected_voters` is guaranteed to be below
//! // `voter_bounds`.
//! debug_assert!(
//! selected_voters.encoded_size() <=
//! SizeTracker::<Staking>::final_byte_size_of(size_tracker.num_voters, size_tracker.size)
//! );
//! ```
//!
//! ### Implementation Details
//!
//! The current implementation of the static tracker is tightly coupled with the staking pallet
//! implementation, namely the representation of a voter ([`VoterOf`]). The SCALE encoded byte size
//! is calculated using [`Encode::size_hint`] of each type in the voter tuple. Each voter's byte
//! size is the sum of:
//! - 1 * [`Encode::size_hint`] of the `AccountId` type;
//! - 1 * [`Encode::size_hint`] of the `VoteWeight` type;
//! - `num_votes` * [`Encode::size_hint`] of the `AccountId` type.
use codec::Encode;
use pezframe_election_provider_support::{
bounds::{DataProviderBounds, SizeBound},
ElectionDataProvider, VoterOf,
};
/// Keeps track of the SCALE encoded byte length of the snapshot's voters or targets.
///
/// The tracker calculates the bytes used based on static rules, without requiring any actual
/// encoding or extra allocations.
#[derive(Clone, Copy, Debug)]
pub struct StaticTracker<DataProvider> {
pub size: usize,
pub counter: usize,
_marker: core::marker::PhantomData<DataProvider>,
}
impl<DataProvider> Default for StaticTracker<DataProvider> {
fn default() -> Self {
Self { size: 0, counter: 0, _marker: Default::default() }
}
}
impl<DataProvider> StaticTracker<DataProvider>
where
DataProvider: ElectionDataProvider,
{
/// Tries to register a new voter.
///
/// If the new voter exhausts the provided bounds, return an error. Otherwise, the internal
/// state of the tracker is updated with the new registered voter.
pub fn try_register_voter(
&mut self,
voter: &VoterOf<DataProvider>,
bounds: &DataProviderBounds,
) -> Result<(), ()> {
let tracker_size_after = {
let voter_hint = Self::voter_size_hint(voter);
Self::final_byte_size_of(self.counter + 1, self.size.saturating_add(voter_hint))
};
match bounds.size_exhausted(SizeBound(tracker_size_after as u32)) {
true => Err(()),
false => {
self.size = tracker_size_after;
self.counter += 1;
Ok(())
},
}
}
/// Calculates the size of the voter to register based on [`Encode::size_hint`].
fn voter_size_hint(voter: &VoterOf<DataProvider>) -> usize {
let (voter_account, vote_weight, targets) = voter;
voter_account
.size_hint()
.saturating_add(vote_weight.size_hint())
.saturating_add(voter_account.size_hint().saturating_mul(targets.len()))
}
/// Tries to register a new target.
///
/// If the new target exhausts the provided bounds, return an error. Otherwise, the internal
/// state of the tracker is updated with the new registered target.
pub fn try_register_target(
&mut self,
target: DataProvider::AccountId,
bounds: &DataProviderBounds,
) -> Result<(), ()> {
let tracker_size_after = Self::final_byte_size_of(
self.counter + 1,
self.size.saturating_add(target.size_hint()),
);
match bounds.size_exhausted(SizeBound(tracker_size_after as u32)) {
true => Err(()),
false => {
self.size = tracker_size_after;
self.counter += 1;
Ok(())
},
}
}
/// Size of the SCALE encoded prefix with a given length.
#[inline]
fn length_prefix(len: usize) -> usize {
use codec::{Compact, CompactLen};
Compact::<u32>::compact_len(&(len as u32))
}
/// Calculates the final size in bytes of the SCALE encoded snapshot voter struct.
fn final_byte_size_of(num_voters: usize, size: usize) -> usize {
Self::length_prefix(num_voters).saturating_add(size)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
mock::{AccountId, Staking, Test},
BoundedVec, MaxNominationsOf,
};
use pezframe_election_provider_support::bounds::ElectionBoundsBuilder;
use pezsp_core::bounded_vec;
type Voters = BoundedVec<AccountId, MaxNominationsOf<Test>>;
#[test]
pub fn election_size_tracker_works() {
let mut voters: Vec<(u64, u64, Voters)> = vec![];
let mut size_tracker = StaticTracker::<Staking>::default();
let voter_bounds = ElectionBoundsBuilder::default().voters_size(1_50.into()).build().voters;
// register 1 voter with 1 vote.
let voter = (1, 10, bounded_vec![2]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
// register another voter, now with 3 votes.
let voter = (2, 20, bounded_vec![3, 4, 5]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
// register noop vote (unlikely to happen).
let voter = (3, 30, bounded_vec![]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
}
#[test]
pub fn election_size_tracker_bounds_works() {
let mut voters: Vec<(u64, u64, Voters)> = vec![];
let mut size_tracker = StaticTracker::<Staking>::default();
let voter_bounds = ElectionBoundsBuilder::default().voters_size(1_00.into()).build().voters;
let voter = (1, 10, bounded_vec![2]);
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_ok());
voters.push(voter);
assert_eq!(
StaticTracker::<Staking>::final_byte_size_of(size_tracker.counter, size_tracker.size),
voters.encoded_size()
);
assert!(size_tracker.size > 0 && size_tracker.size < 1_00);
let size_before_overflow = size_tracker.size;
// try many voters that will overflow the tracker's buffer.
let voter = (2, 10, bounded_vec![2, 3, 4, 5, 6, 7, 8, 9]);
voters.push(voter.clone());
assert!(size_tracker.try_register_voter(&voter, &voter_bounds).is_err());
assert!(size_tracker.size > 0 && size_tracker.size < 1_00);
// size of the tracker did not update when trying to register votes failed.
assert_eq!(size_tracker.size, size_before_overflow);
}
#[test]
fn len_prefix_works() {
let length_samples =
vec![0usize, 1, 62, 63, 64, 16383, 16384, 16385, 1073741822, 1073741823, 1073741824];
for s in length_samples {
// the encoded size of a vector of n bytes should be n + the length prefix
assert_eq!(vec![1u8; s].encoded_size(), StaticTracker::<Staking>::length_prefix(s) + s);
}
}
}
@@ -0,0 +1,590 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A Ledger implementation for stakers.
//!
//! A [`StakingLedger`] encapsulates all the state and logic related to the stake of bonded
//! stakers, namely, it handles the following storage items:
//! * [`Bonded`]: mutates and reads the state of the controller <> stash bond map (to be deprecated
//! soon);
//! * [`Ledger`]: mutates and reads the state of all the stakers. The [`Ledger`] storage item stores
//! instances of [`StakingLedger`] keyed by the staker's controller account and should be mutated
//! and read through the [`StakingLedger`] API;
//! * [`Payee`]: mutates and reads the reward destination preferences for a bonded stash.
//! * Staking locks: mutates the locks for staking.
//!
//! NOTE: All the storage operations related to the staking ledger (both reads and writes) *MUST* be
//! performed through the methods exposed by the [`StakingLedger`] implementation in order to ensure
//! state consistency.
use crate::{
asset, log, BalanceOf, Bonded, Config, DecodeWithMemTracking, Error, Ledger, Pallet, Payee,
RewardDestination, Vec, VirtualStakers,
};
use alloc::{collections::BTreeMap, fmt::Debug};
use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use pezframe_support::{
defensive, ensure,
traits::{Defensive, DefensiveSaturating, Get},
BoundedVec, CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound,
};
use scale_info::TypeInfo;
use pezsp_runtime::{traits::Zero, DispatchResult, Perquintill, Rounding, Saturating};
use pezsp_staking::{EraIndex, OnStakingUpdate, StakingAccount, StakingInterface};
/// Just a Balance/BlockNumber tuple to encode when a chunk of funds will be unlocked.
#[derive(
PartialEq, Eq, Clone, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo, MaxEncodedLen,
)]
pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> {
/// Amount of funds to be unlocked.
#[codec(compact)]
pub value: Balance,
/// Era number at which point it'll be unlocked.
#[codec(compact)]
pub era: EraIndex,
}
/// The ledger of a (bonded) stash.
///
/// Note: All the reads and mutations to the [`Ledger`], [`Bonded`] and [`Payee`] storage items
/// *MUST* be performed through the methods exposed by this struct, to ensure the consistency of
/// ledger's data and corresponding staking lock
///
/// TODO: move struct definition and full implementation into `/src/ledger.rs`. Currently
/// leaving here to enforce a clean PR diff, given how critical this logic is. Tracking issue
/// <https://github.com/pezkuwichain/kurdistan-sdk/issues/21>.
#[derive(
PartialEqNoBound,
EqNoBound,
CloneNoBound,
Encode,
Decode,
DebugNoBound,
TypeInfo,
MaxEncodedLen,
DecodeWithMemTracking,
)]
#[scale_info(skip_type_params(T))]
pub struct StakingLedger<T: Config> {
/// The stash account whose balance is actually locked and at stake.
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: BalanceOf<T>,
/// The total amount of the stash's balance that will be at stake in any forthcoming
/// rounds.
#[codec(compact)]
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<BalanceOf<T>>, T::MaxUnlockingChunks>,
/// The controller associated with this ledger's stash.
///
/// This is not stored on-chain, and is only bundled when the ledger is read from storage.
/// Use [`Self::controller()`] function to get the controller associated with the ledger.
#[codec(skip)]
pub controller: Option<T::AccountId>,
}
impl<T: Config> StakingLedger<T> {
#[cfg(any(feature = "runtime-benchmarks", test))]
pub fn default_from(stash: T::AccountId) -> Self {
Self {
stash: stash.clone(),
total: Zero::zero(),
active: Zero::zero(),
unlocking: Default::default(),
controller: Some(stash),
}
}
/// Returns a new instance of a staking ledger.
///
/// The [`Ledger`] storage is not mutated. In order to store, `StakingLedger::update` must be
/// called on the returned staking ledger.
///
/// Note: as the controller accounts are being deprecated, the stash account is the same as the
/// controller account.
pub fn new(stash: T::AccountId, stake: BalanceOf<T>) -> Self {
Self {
stash: stash.clone(),
active: stake,
total: stake,
unlocking: Default::default(),
// controllers are deprecated and mapped 1-1 to stashes.
controller: Some(stash),
}
}
/// Returns the paired account, if any.
///
/// A "pair" refers to the tuple (stash, controller). If the input is a
/// [`StakingAccount::Stash`] variant, its pair account will be of type
/// [`StakingAccount::Controller`] and vice-versa.
///
/// This method is meant to abstract from the runtime development the difference between stash
/// and controller. This will be deprecated once the controller is fully deprecated as well.
pub(crate) fn paired_account(account: StakingAccount<T::AccountId>) -> Option<T::AccountId> {
match account {
StakingAccount::Stash(stash) => <Bonded<T>>::get(stash),
StakingAccount::Controller(controller) =>
<Ledger<T>>::get(&controller).map(|ledger| ledger.stash),
}
}
/// Returns whether a given account is bonded.
pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool {
match account {
StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash),
StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller),
}
}
/// Returns a staking ledger, if it is bonded and it exists in storage.
///
/// This getter can be called with either a controller or stash account, provided that the
/// account is properly wrapped in the respective [`StakingAccount`] variant. This is meant to
/// abstract the concept of controller/stash accounts from the caller.
///
/// Returns [`Error::BadState`] when a bond is in "bad state". A bond is in a bad state when a
/// stash has a controller which is bonding a ledger associated with another stash.
pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
let (stash, controller) = match account {
StakingAccount::Stash(stash) =>
(stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?),
StakingAccount::Controller(controller) => (
Ledger::<T>::get(&controller)
.map(|l| l.stash)
.ok_or(Error::<T>::NotController)?,
controller,
),
};
let ledger = <Ledger<T>>::get(&controller)
.map(|mut ledger| {
ledger.controller = Some(controller.clone());
ledger
})
.ok_or(Error::<T>::NotController)?;
// if ledger bond is in a bad state, return error to prevent applying operations that may
// further spoil the ledger's state. A bond is in bad state when the bonded controller is
// associated with a different ledger (i.e. a ledger with a different stash).
//
// See <https://github.com/pezkuwichain/pezkuwi-sdk/issues/128> for more details.
ensure!(
Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
Error::<T>::BadState
);
Ok(ledger)
}
/// Returns the reward destination of a staking ledger, stored in [`Payee`].
///
/// Note: if the stash is not bonded and/or does not have an entry in [`Payee`], it returns the
/// default reward destination.
pub(crate) fn reward_destination(
account: StakingAccount<T::AccountId>,
) -> Option<RewardDestination<T::AccountId>> {
let stash = match account {
StakingAccount::Stash(stash) => Some(stash),
StakingAccount::Controller(controller) =>
Self::paired_account(StakingAccount::Controller(controller)),
};
if let Some(stash) = stash {
<Payee<T>>::get(stash)
} else {
defensive!("fetched reward destination from unbonded stash {}", stash);
None
}
}
/// Returns the controller account of a staking ledger.
///
/// Note: it will fallback into querying the [`Bonded`] storage with the ledger stash if the
/// controller is not set in `self`, which most likely means that self was fetched directly from
/// [`Ledger`] instead of through the methods exposed in [`StakingLedger`]. If the ledger does
/// not exist in storage, it returns `None`.
pub fn controller(&self) -> Option<T::AccountId> {
self.controller.clone().or_else(|| {
defensive!("fetched a controller on a ledger instance without it.");
Self::paired_account(StakingAccount::Stash(self.stash.clone()))
})
}
/// Inserts/updates a staking ledger account.
///
/// Bonds the ledger if it is not bonded yet, signalling that this is a new ledger. The staking
/// lock/hold of the stash account are updated accordingly.
///
/// Note: To ensure lock consistency, all the [`Ledger`] storage updates should be made through
/// this helper function.
pub(crate) fn update(self) -> Result<(), Error<T>> {
if !<Bonded<T>>::contains_key(&self.stash) {
return Err(Error::<T>::NotStash);
}
// We skip locking virtual stakers.
if !Pallet::<T>::is_virtual_staker(&self.stash) {
// for direct stakers, update lock on stash based on ledger.
asset::update_stake::<T>(&self.stash, self.total)
.map_err(|_| Error::<T>::NotEnoughFunds)?;
}
Ledger::<T>::insert(
&self.controller().ok_or_else(|| {
defensive!("update called on a ledger that is not bonded.");
Error::<T>::NotController
})?,
&self,
);
Ok(())
}
/// Bonds a ledger.
///
/// It sets the reward preferences for the bonded stash.
pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
if <Bonded<T>>::contains_key(&self.stash) {
return Err(Error::<T>::AlreadyBonded);
}
<Payee<T>>::insert(&self.stash, payee);
<Bonded<T>>::insert(&self.stash, &self.stash);
self.update()
}
/// Sets the ledger Payee.
pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
if !<Bonded<T>>::contains_key(&self.stash) {
return Err(Error::<T>::NotStash);
}
<Payee<T>>::insert(&self.stash, payee);
Ok(())
}
/// Sets the ledger controller to its stash.
pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> {
let controller = self.controller.as_ref()
.defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.")
.ok_or(Error::<T>::NotController)?;
ensure!(self.stash != *controller, Error::<T>::AlreadyPaired);
// check if the ledger's stash is a controller of another ledger.
if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
// there is a ledger bonded by the stash. In this case, the stash of the bonded ledger
// should be the same as the ledger's stash. Otherwise fail to prevent data
// inconsistencies. See <https://github.com/pezkuwichain/kurdistan-sdk/issues/117> for more
// details.
ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState);
}
<Ledger<T>>::remove(&controller);
<Ledger<T>>::insert(&self.stash, &self);
<Bonded<T>>::insert(&self.stash, &self.stash);
Ok(())
}
/// Clears all data related to a staking ledger and its bond in both [`Ledger`] and [`Bonded`]
/// storage items and updates the stash staking lock.
pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult {
let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
<Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
Ledger::<T>::remove(controller);
<Bonded<T>>::remove(&stash);
<Payee<T>>::remove(&stash);
// kill virtual staker if it exists.
if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
// if not virtual staker, clear locks.
asset::kill_stake::<T>(&ledger.stash)?;
}
Pallet::<T>::deposit_event(crate::Event::<T>::StakerRemoved {
stash: ledger.stash.clone(),
});
Ok(())
})?
}
#[cfg(test)]
pub(crate) fn assert_stash_killed(stash: T::AccountId) {
assert!(!Ledger::<T>::contains_key(&stash));
assert!(!Bonded::<T>::contains_key(&stash));
assert!(!Payee::<T>::contains_key(&stash));
assert!(!VirtualStakers::<T>::contains_key(&stash));
}
/// Remove entries from `unlocking` that are sufficiently old and reduce the
/// total by the sum of their balances.
pub(crate) fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
let mut total = self.total;
let unlocking: BoundedVec<_, _> = self
.unlocking
.into_iter()
.filter(|chunk| {
if chunk.era > current_era {
true
} else {
total = total.saturating_sub(chunk.value);
false
}
})
.collect::<Vec<_>>()
.try_into()
.expect(
"filtering items from a bounded vec always leaves length less than bounds. qed",
);
Self {
stash: self.stash,
total,
active: self.active,
unlocking,
controller: self.controller,
}
}
/// Re-bond funds that were scheduled for unlocking.
///
/// Returns the updated ledger, and the amount actually rebonded.
pub(crate) 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.defensive_saturating_add(last.value) <= value {
unlocking_balance += last.value;
self.active += last.value;
self.unlocking.pop();
} else {
let diff = value.defensive_saturating_sub(unlocking_balance);
unlocking_balance += diff;
self.active += diff;
last.value -= diff;
}
if unlocking_balance >= value {
break;
}
}
(self, unlocking_balance)
}
/// Slash the staker for a given amount of balance.
///
/// This implements a proportional slashing system, whereby we set our preference to slash as
/// such:
///
/// - If any unlocking chunks exist that are scheduled to be unlocked at `slash_era +
/// bonding_duration` and onwards, the slash is divided equally between the active ledger and
/// the unlocking chunks.
/// - If no such chunks exist, then only the active balance is slashed.
///
/// Note that the above is only a *preference*. If for any reason the active ledger, with or
/// without some portion of the unlocking chunks that are more justified to be slashed are not
/// enough, then the slashing will continue and will consume as much of the active and unlocking
/// chunks as needed.
///
/// This will never slash more than the given amount. If any of the chunks become dusted, the
/// last chunk is slashed slightly less to compensate. Returns the amount of funds actually
/// slashed.
///
/// `slash_era` is the era in which the slash (which is being enacted now) actually happened.
///
/// This calls `Config::OnStakingUpdate::on_slash` with information as to how the slash was
/// applied.
pub fn slash(
&mut self,
slash_amount: BalanceOf<T>,
minimum_balance: BalanceOf<T>,
slash_era: EraIndex,
) -> BalanceOf<T> {
if slash_amount.is_zero() {
return Zero::zero();
}
use pezsp_runtime::PerThing as _;
let mut remaining_slash = slash_amount;
let pre_slash_total = self.total;
// for a `slash_era = x`, any chunk that is scheduled to be unlocked at era `x + 28`
// (assuming 28 is the bonding duration) onwards should be slashed.
let slashable_chunks_start = slash_era.saturating_add(T::BondingDuration::get());
// `Some(ratio)` if this is proportional, with `ratio`, `None` otherwise. In both cases, we
// slash first the active chunk, and then `slash_chunks_priority`.
let (maybe_proportional, slash_chunks_priority) = {
if let Some(first_slashable_index) =
self.unlocking.iter().position(|c| c.era >= slashable_chunks_start)
{
// If there exists a chunk who's after the first_slashable_start, then this is a
// proportional slash, because we want to slash active and these chunks
// proportionally.
// 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 = first_slashable_index..self.unlocking.len();
let unbonding_affected_balance =
affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
if let Some(chunk) = self.unlocking.get(i).defensive() {
sum.saturating_add(chunk.value)
} else {
sum
}
});
let affected_balance = self.active.saturating_add(unbonding_affected_balance);
let ratio = Perquintill::from_rational_with_rounding(
slash_amount,
affected_balance,
Rounding::Up,
)
.unwrap_or_else(|_| Perquintill::one());
(
Some(ratio),
affected_indices.chain((0..first_slashable_index).rev()).collect::<Vec<_>>(),
)
} else {
// We just slash from the last chunk to the most recent one, if need be.
(None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
}
};
// Helper to update `target` and the ledgers total after accounting for slashing `target`.
log!(
trace,
"slashing {:?} for era {:?} out of {:?}, priority: {:?}, proportional = {:?}",
slash_amount,
slash_era,
self,
slash_chunks_priority,
maybe_proportional,
);
let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
let mut slash_from_target = if let Some(ratio) = maybe_proportional {
ratio.mul_ceil(*target)
} else {
*slash_remaining
}
// this is the total that that the slash target has. We can't slash more than
// this anyhow!
.min(*target)
// this is the total amount that we would have wanted to slash
// non-proportionally, a proportional slash should never exceed this either!
.min(*slash_remaining);
// slash out from *target exactly `slash_from_target`.
*target = *target - slash_from_target;
if *target < minimum_balance {
// Slash the rest of the target if it's dust. This might cause the last chunk to be
// slightly under-slashed, by at most `MaxUnlockingChunks * ED`, which is not a big
// deal.
slash_from_target =
core::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 remaining_slash.is_zero() {
break;
}
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);
} else {
break;
}
}
// clean unlocking chunks that are set to zero.
self.unlocking.retain(|c| !c.value.is_zero());
let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
T::EventListeners::on_slash(
&self.stash,
self.active,
&slashed_unlocking,
final_slashed_amount,
);
final_slashed_amount
}
}
/// State of a ledger with regards with its data and metadata integrity.
#[derive(PartialEq, Debug)]
pub(crate) enum LedgerIntegrityState {
/// Ledger, bond and corresponding staking lock is OK.
Ok,
/// Ledger and/or bond is corrupted. This means that the bond has a ledger with a different
/// stash than the bonded stash.
Corrupted,
/// Ledger was corrupted and it has been killed.
CorruptedKilled,
/// Ledger and bond are OK, however the ledger's stash lock is out of sync.
LockCorrupted,
}
// This structs makes it easy to write tests to compare staking ledgers fetched from storage. This
// is required because the controller field is not stored in storage and it is private.
#[cfg(test)]
#[derive(pezframe_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct StakingLedgerInspect<T: Config> {
pub stash: T::AccountId,
#[codec(compact)]
pub total: BalanceOf<T>,
#[codec(compact)]
pub active: BalanceOf<T>,
pub unlocking:
pezframe_support::BoundedVec<crate::UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
}
#[cfg(test)]
impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> {
fn eq(&self, other: &StakingLedgerInspect<T>) -> bool {
self.stash == other.stash &&
self.total == other.total &&
self.active == other.active &&
self.unlocking == other.unlocking
}
}
#[cfg(test)]
impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}
@@ -0,0 +1,576 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Staking Async Pallet
//!
//! This pallet is a fork of the original `pezpallet-staking`, with a number of key differences:
//!
//! * It no longer has access to a secure timestamp, previously used to calculate the duration of an
//! era.
//! * It no longer has access to a pezpallet-session.
//! * It no longer has access to a pezpallet-authorship.
//! * It is capable of working with a multi-page `ElectionProvider``, aka.
//! `pezpallet-election-provider-multi-block`.
//!
//! While `pezpallet-staking` was somewhat general-purpose, this pallet is absolutely NOT right from
//! the get-go: It is designed to be used ONLY in Pezkuwi/Kusama AssetHub system teyrchains.
//!
//! The workings of this pallet can be divided into a number of subsystems, as follows.
//!
//! ## User Interactions
//!
//! TODO
//!
//! ## Session and Era Rotation
//!
//! TODO
//!
//! ## Exposure Collection
//!
//! TODO
//!
//! ## Slashing Pipeline and Withdrawal Restrictions
//!
//! This pallet implements a robust slashing mechanism that ensures the integrity of the staking
//! system while preventing stakers from withdrawing funds that might still be subject to slashing.
//!
//! ### Overview of the Slashing Pipeline
//!
//! The slashing process consists of multiple phases:
//!
//! 1. **Offence Reporting**: Offences are reported from the relay chain through `on_new_offences`
//! 2. **Queuing**: Valid offences are added to the `OffenceQueue` for processing
//! 3. **Processing**: Offences are processed incrementally over multiple blocks
//! 4. **Application**: Slashes are either applied immediately or deferred based on configuration
//!
//! ### Phase 1: Offence Reporting
//!
//! Offences are reported from the relay chain (e.g., from BABE, GRANDPA, BEEFY, or teyrchain
//! modules) through the `on_new_offences` function:
//!
//! ```text
//! struct Offence {
//! offender: AccountId, // The validator being slashed
//! reporters: Vec<AccountId>, // Who reported the offence (may be empty)
//! slash_fraction: Perbill, // Percentage of stake to slash
//! }
//! ```
//!
//! **Reporting Deadlines**:
//! - With deferred slashing: Offences must be reported within `SlashDeferDuration - 1` eras
//! - With immediate slashing: Offences can be reported up to `BondingDuration` eras old
//!
//! Example: If `SlashDeferDuration = 27` and current era is 100:
//! - Oldest reportable offence: Era 74 (100 - 26)
//! - Offences from era 73 or earlier are rejected
//!
//! ### Phase 2: Queuing
//!
//! When an offence passes validation, it's added to the queue:
//!
//! 1. **Storage**: Added to `OffenceQueue`: `(EraIndex, AccountId) -> OffenceRecord`
//! 2. **Era Tracking**: Era added to `OffenceQueueEras` (sorted vector of eras with offences)
//! 3. **Duplicate Handling**: If an offence already exists for the same validator in the same era,
//! only the higher slash fraction is kept
//!
//! ### Phase 3: Processing
//!
//! Offences are processed incrementally in `on_initialize` each block:
//!
//! ```text
//! 1. Load oldest offence from queue
//! 2. Move to `ProcessingOffence` storage
//! 3. For each exposure page (from last to first):
//! - Calculate slash for validator's own stake
//! - Calculate slash for each nominator (pro-rata based on exposure)
//! - Track total slash and reward amounts
//! 4. Once all pages processed, create `UnappliedSlash`
//! ```
//!
//! **Key Features**:
//! - **Page-by-page processing**: Large validator sets don't overwhelm a single block
//! - **Pro-rata slashing**: Nominators slashed proportionally to their stake
//! - **Reward calculation**: A portion goes to reporters (if any)
//!
//! ### Phase 4: Application
//!
//! Based on `SlashDeferDuration`, slashes are either:
//!
//! **Immediate (SlashDeferDuration = 0)**:
//! - Applied right away in the same block
//! - Funds deducted from staking ledger immediately
//!
//! **Deferred (SlashDeferDuration > 0)**:
//! - Stored in `UnappliedSlashes` for future application
//! - Applied at era: `offence_era + SlashDeferDuration`
//! - Can be cancelled by governance before application
//!
//! ### Storage Items Involved
//!
//! - `OffenceQueue`: Pending offences to process
//! - `OffenceQueueEras`: Sorted list of eras with offences
//! - `ProcessingOffence`: Currently processing offence
//! - `ValidatorSlashInEra`: Tracks highest slash per validator per era
//! - `UnappliedSlashes`: Deferred slashes waiting for application
//!
//! ### Withdrawal Restrictions
//!
//! To maintain slashing guarantees, withdrawals are restricted:
//!
//! **Withdrawal Era Calculation**:
//! ```text
//! earliest_era_to_withdraw = min(
//! active_era,
//! last_fully_processed_offence_era + BondingDuration
//! )
//! ```
//!
//! **Example**:
//! - Active era: 100
//! - Oldest unprocessed offence: Era 70
//! - BondingDuration: 28
//! - Withdrawal allowed only for chunks with era ≤ 97 (70 - 1 + 28)
//!
//! **Withdrawal Timeline Example with an Offence**:
//! ```text
//! Era: 90 91 92 93 94 95 96 97 98 99 100 ... 117 118
//! | | | | | | | | | | | | |
//! Unbond: U
//! Offence: X
//! Reported: R
//! Processed: P (within next few blocks)
//! Slash Applied: S
//! Withdraw: ❌ ✓
//!
//! With BondingDuration = 28 and SlashDeferDuration = 27:
//! - User unbonds in era 90
//! - Offence occurs in era 90
//! - Reported in era 92 (typically within 2 days, but reportable until Era 116)
//! - Processed in era 92 (within next few blocks after reporting)
//! - Slash deferred for 27 eras, applied at era 117 (90 + 27)
//! - Cannot withdraw unbonded chunks until era 118 (90 + 28)
//!
//! The 28-era bonding duration ensures that any offences committed before or during
//! unbonding have time to be reported, processed, and applied before funds can be
//! withdrawn. This provides a window for governance to cancel slashes that may have
//! resulted from software bugs.
//! ```
//!
//! **Key Restrictions**:
//! 1. Cannot withdraw if previous era has unapplied slashes
//! 2. Cannot withdraw funds from eras with unprocessed offences
#![cfg_attr(not(feature = "std"), no_std)]
#![recursion_limit = "256"]
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
#[cfg(any(feature = "runtime-benchmarks", test))]
pub mod testing_utils;
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod tests;
pub mod asset;
pub mod election_size_tracker;
pub mod ledger;
mod pallet;
pub mod session_rotation;
pub mod slashing;
pub mod weights;
extern crate alloc;
use alloc::{vec, vec::Vec};
use codec::{Decode, DecodeWithMemTracking, Encode, HasCompact, MaxEncodedLen};
use pezframe_election_provider_support::ElectionProvider;
use pezframe_support::{
traits::{
tokens::fungible::{Credit, Debt},
ConstU32, Contains, Get, LockIdentifier,
},
BoundedVec, DebugNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
WeakBoundedVec,
};
use pezframe_system::pezpallet_prelude::BlockNumberFor;
use ledger::LedgerIntegrityState;
use scale_info::TypeInfo;
use pezsp_runtime::{
traits::{AtLeast32BitUnsigned, One, StaticLookup, UniqueSaturatedInto},
BoundedBTreeMap, Perbill, RuntimeDebug, Saturating,
};
use pezsp_staking::{EraIndex, ExposurePage, PagedExposureMetadata, SessionIndex};
pub use pezsp_staking::{Exposure, IndividualExposure, StakerStatus};
pub use weights::WeightInfo;
// public exports
pub use ledger::{StakingLedger, UnlockChunk};
pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap};
pub(crate) const STAKING_ID: LockIdentifier = *b"staking ";
pub(crate) const LOG_TARGET: &str = "runtime::staking-async";
// syntactic sugar for logging.
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: crate::LOG_TARGET,
concat!("[{:?}] 💸 ", $patter), <pezframe_system::Pallet<T>>::block_number() $(, $values)*
)
};
}
/// Alias for a bounded set of exposures behind a validator, parameterized by this pallet's
/// election provider.
pub type BoundedExposuresOf<T> = BoundedVec<
(
<T as pezframe_system::Config>::AccountId,
Exposure<<T as pezframe_system::Config>::AccountId, BalanceOf<T>>,
),
MaxWinnersPerPageOf<<T as Config>::ElectionProvider>,
>;
/// Alias for the maximum number of winners (aka. active validators), as defined in by this pallet's
/// config.
pub type MaxWinnersOf<T> = <T as Config>::MaxValidatorSet;
/// Alias for the maximum number of winners per page, as expected by the election provider.
pub type MaxWinnersPerPageOf<P> = <P as ElectionProvider>::MaxWinnersPerPage;
/// Maximum number of nominations per nominator.
pub type MaxNominationsOf<T> =
<<T as Config>::NominationsQuota as NominationsQuota<BalanceOf<T>>>::MaxNominations;
/// Counter for the number of "reward" points earned by a given validator.
pub type RewardPoint = u32;
/// The balance type of this pallet.
pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
type PositiveImbalanceOf<T> = Debt<<T as pezframe_system::Config>::AccountId, <T as Config>::Currency>;
pub type NegativeImbalanceOf<T> =
Credit<<T as pezframe_system::Config>::AccountId, <T as Config>::Currency>;
type AccountIdLookupOf<T> = <<T as pezframe_system::Config>::Lookup as StaticLookup>::Source;
/// Information regarding the active era (era in used in session).
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, PartialEq, Eq, Clone)]
pub struct ActiveEraInfo {
/// Index of era.
pub index: EraIndex,
/// Moment of start expressed as millisecond from `$UNIX_EPOCH`.
///
/// Start can be none if start hasn't been set for the era yet,
/// Start is set on the first on_finalize of the era to guarantee usage of `Time`.
pub start: Option<u64>,
}
/// 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.
#[derive(
PartialEqNoBound, Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen, DefaultNoBound,
)]
#[codec(mel_bound())]
#[scale_info(skip_type_params(T))]
pub struct EraRewardPoints<T: Config> {
/// Total number of points. Equals the sum of reward points for each validator.
pub total: RewardPoint,
/// The reward points earned by a given validator.
pub individual: BoundedBTreeMap<T::AccountId, RewardPoint, T::MaxValidatorSet>,
}
/// A destination account for payment.
#[derive(
PartialEq,
Eq,
Copy,
Clone,
Encode,
Decode,
DecodeWithMemTracking,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub enum RewardDestination<AccountId> {
/// Pay into the stash account, increasing the amount at stake accordingly.
Staked,
/// Pay into the stash account, not increasing the amount at stake.
Stash,
#[deprecated(
note = "`Controller` will be removed after January 2024. Use `Account(controller)` instead."
)]
Controller,
/// Pay into a specified account.
Account(AccountId),
/// Receive no reward.
None,
}
/// Preference of what happens regarding validation.
#[derive(
PartialEq,
Eq,
Clone,
Encode,
Decode,
DecodeWithMemTracking,
RuntimeDebug,
TypeInfo,
Default,
MaxEncodedLen,
)]
pub struct ValidatorPrefs {
/// Reward that validator takes up-front; only the rest is split between themselves and
/// nominators.
#[codec(compact)]
pub commission: Perbill,
/// Whether or not this validator is accepting more nominations. If `true`, then no nominator
/// who is not already nominating this validator may nominate them. By default, validators
/// are accepting nominations.
pub blocked: bool,
}
/// Status of a paged snapshot progress.
#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Default)]
pub enum SnapshotStatus<AccountId> {
/// Paged snapshot is in progress, the `AccountId` was the last staker iterated in the list.
Ongoing(AccountId),
/// All the stakers in the system have been consumed since the snapshot started.
Consumed,
/// Waiting for a new snapshot to be requested.
#[default]
Waiting,
}
/// A record of the nominations made by a specific account.
#[derive(
PartialEqNoBound, EqNoBound, Clone, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,
)]
#[codec(mel_bound())]
#[scale_info(skip_type_params(T))]
pub struct Nominations<T: Config> {
/// The targets of nomination.
pub targets: BoundedVec<T::AccountId, MaxNominationsOf<T>>,
/// The era the nominations were submitted.
///
/// Except for initial nominations which are considered submitted at era 0.
pub submitted_in: EraIndex,
/// Whether the nominations have been suppressed. This can happen due to slashing of the
/// validators, or other events that might invalidate the nomination.
///
/// NOTE: this for future proofing and is thus far not used.
pub suppressed: bool,
}
/// Facade struct to encapsulate `PagedExposureMetadata` and a single page of `ExposurePage`.
///
/// This is useful where we need to take into account the validator's own stake and total exposure
/// in consideration, in addition to the individual nominators backing them.
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)]
pub struct PagedExposure<AccountId, Balance: HasCompact + codec::MaxEncodedLen> {
exposure_metadata: PagedExposureMetadata<Balance>,
exposure_page: ExposurePage<AccountId, Balance>,
}
impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEncodedLen>
PagedExposure<AccountId, Balance>
{
/// Create a new instance of `PagedExposure` from legacy clipped exposures.
pub fn from_clipped(exposure: Exposure<AccountId, Balance>) -> Self {
Self {
exposure_metadata: PagedExposureMetadata {
total: exposure.total,
own: exposure.own,
nominator_count: exposure.others.len() as u32,
page_count: 1,
},
exposure_page: ExposurePage { page_total: exposure.total, others: exposure.others },
}
}
/// Returns total exposure of this validator across pages
pub fn total(&self) -> Balance {
self.exposure_metadata.total
}
/// Returns total exposure of this validator for the current page
pub fn page_total(&self) -> Balance {
self.exposure_page.page_total + self.exposure_metadata.own
}
/// Returns validator's own stake that is exposed
pub fn own(&self) -> Balance {
self.exposure_metadata.own
}
/// Returns the portions of nominators stashes that are exposed in this page.
pub fn others(&self) -> &Vec<IndividualExposure<AccountId, Balance>> {
&self.exposure_page.others
}
}
/// A pending slash record. The value of the slash has been computed but not applied yet,
/// rather deferred for several eras.
#[derive(Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen, PartialEqNoBound, EqNoBound)]
#[scale_info(skip_type_params(T))]
pub struct UnappliedSlash<T: Config> {
/// The stash ID of the offending validator.
pub validator: T::AccountId,
/// The validator's own slash.
pub own: BalanceOf<T>,
/// All other slashed stakers and amounts.
pub others: WeakBoundedVec<(T::AccountId, BalanceOf<T>), T::MaxExposurePageSize>,
/// Reporters of the offence; bounty payout recipients.
pub reporter: Option<T::AccountId>,
/// The amount of payout.
pub payout: BalanceOf<T>,
}
/// Something that defines the maximum number of nominations per nominator based on a curve.
///
/// The method `curve` implements the nomination quota curve and should not be used directly.
/// However, `get_quota` returns the bounded maximum number of nominations based on `fn curve` and
/// the nominator's balance.
pub trait NominationsQuota<Balance> {
/// Strict maximum number of nominations that caps the nominations curve. This value can be
/// used as the upper bound of the number of votes per nominator.
type MaxNominations: Get<u32>;
/// Returns the voter's nomination quota within reasonable bounds [`min`, `max`], where `min`
/// is 1 and `max` is `Self::MaxNominations`.
fn get_quota(balance: Balance) -> u32 {
Self::curve(balance).clamp(1, Self::MaxNominations::get())
}
/// Returns the voter's nomination quota based on its balance and a curve.
fn curve(balance: Balance) -> u32;
}
/// A nomination quota that allows up to MAX nominations for all validators.
pub struct FixedNominationsQuota<const MAX: u32>;
impl<Balance, const MAX: u32> NominationsQuota<Balance> for FixedNominationsQuota<MAX> {
type MaxNominations = ConstU32<MAX>;
fn curve(_: Balance) -> u32 {
MAX
}
}
/// Handler for determining how much of a balance should be paid out on the current era.
pub trait EraPayout<Balance> {
/// Determine the payout for this era.
///
/// Returns the amount to be paid to stakers in this era, as well as whatever else should be
/// paid out ("the rest").
fn era_payout(
total_staked: Balance,
total_issuance: Balance,
era_duration_millis: u64,
) -> (Balance, Balance);
}
impl<Balance: Default> EraPayout<Balance> for () {
fn era_payout(
_total_staked: Balance,
_total_issuance: Balance,
_era_duration_millis: u64,
) -> (Balance, Balance) {
(Default::default(), Default::default())
}
}
/// Mode of era-forcing.
#[derive(
Copy,
Clone,
PartialEq,
Eq,
Encode,
Decode,
DecodeWithMemTracking,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
serde::Serialize,
serde::Deserialize,
)]
pub enum Forcing {
/// Not forcing anything - just let whatever happen.
NotForcing,
/// Force a new era, then reset to `NotForcing` as soon as it is done.
/// Note that this will force to trigger an election until a new era is triggered, if the
/// election failed, the next session end will trigger a new election again, until success.
ForceNew,
/// Avoid a new era indefinitely.
ForceNone,
/// Force a new era at the end of all sessions indefinitely.
ForceAlways,
}
impl Default for Forcing {
fn default() -> Self {
Forcing::NotForcing
}
}
/// A utility struct that provides a way to check if a given account is a staker.
///
/// This struct implements the `Contains` trait, allowing it to determine whether
/// a particular account is currently staking by checking if the account exists in
/// the staking ledger.
///
/// Intended to be used in [`crate::Config::Filter`].
pub struct AllStakers<T: Config>(core::marker::PhantomData<T>);
impl<T: Config> Contains<T::AccountId> for AllStakers<T> {
/// Checks if the given account ID corresponds to a staker.
///
/// # Returns
/// - `true` if the account has an entry in the staking ledger (indicating it is staking).
/// - `false` otherwise.
fn contains(account: &T::AccountId) -> bool {
Ledger::<T>::contains_key(account)
}
}
/// A smart type to determine the [`Config::PlanningEraOffset`], given:
///
/// * Expected relay session duration, `RS`
/// * Time taking into consideration for XCM sending, `S`
///
/// It will use the estimated election duration, the relay session duration, and add one as it knows
/// the relay chain will want to buffer validators for one session. This is needed because we use
/// this in our calculation based on the "active era".
pub struct PlanningEraOffsetOf<T, RS, S>(core::marker::PhantomData<(T, RS, S)>);
impl<T: Config, RS: Get<BlockNumberFor<T>>, S: Get<BlockNumberFor<T>>> Get<SessionIndex>
for PlanningEraOffsetOf<T, RS, S>
{
fn get() -> SessionIndex {
let election_duration = <T::ElectionProvider as ElectionProvider>::duration_with_export();
let sessions_needed = (election_duration + S::get()) / RS::get();
// add one, because we know the RC session pallet wants to buffer for one session, and
// another one cause we will receive activation report one session after that.
sessions_needed
.saturating_add(One::one())
.saturating_add(One::one())
.unique_saturated_into()
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,497 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A slashing implementation for NPoS systems.
//!
//! For the purposes of the economic model, it is easiest to think of each validator as a nominator
//! which nominates only its own identity.
//!
//! The act of nomination signals intent to unify economic identity with the validator - to take
//! part in the rewards of a job well done, and to take part in the punishment of a job done badly.
//!
//! There are 3 main difficulties to account for with slashing in NPoS:
//! - A nominator can nominate multiple validators and be slashed via any of them.
//! - Until slashed, stake is reused from era to era. Nominating with N coins for E eras in a row
//! does not mean you have N*E coins to be slashed - you've only ever had N.
//! - Slashable offences can be found after the fact and out of order.
//!
//! We only slash participants for the _maximum_ slash they receive in some time period (era),
//! rather than the sum. This ensures a protection from overslashing.
//!
//! In most of the cases, thanks to validator disabling, an offender won't be able to commit more
//! than one offence. An exception is the case when the number of offenders reaches the
//! Byzantine threshold. In that case one or more offenders with the smallest offence will be
//! re-enabled and they can commit another offence. But as noted previously, even in this case we
//! slash the offender only for the biggest offence committed within an era.
//!
//! Based on research at <https://research.web3.foundation/Polkadot/security/slashing/npos>
use crate::{
asset, log, session_rotation::Eras, BalanceOf, Config, NegativeImbalanceOf, OffenceQueue,
OffenceQueueEras, PagedExposure, Pallet, Perbill, ProcessingOffence, SlashRewardFraction,
UnappliedSlash, UnappliedSlashes, ValidatorSlashInEra, WeightInfo,
};
use alloc::vec::Vec;
use codec::{Decode, Encode, MaxEncodedLen};
use pezframe_support::traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced};
use scale_info::TypeInfo;
use pezsp_runtime::{
traits::{Saturating, Zero},
RuntimeDebug, WeakBoundedVec, Weight,
};
use pezsp_staking::{EraIndex, StakingInterface};
/// Parameters for performing a slash.
#[derive(Clone)]
pub(crate) struct SlashParams<'a, T: 'a + Config> {
/// The stash account being slashed.
pub(crate) stash: &'a T::AccountId,
/// The proportion of the slash.
pub(crate) slash: Perbill,
/// The prior slash proportion of the validator if the validator has been reported multiple
/// times in the same era, and a new greater slash replaces the old one.
/// Invariant: slash > prior_slash
pub(crate) prior_slash: Perbill,
/// The exposure of the stash and all nominators.
pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
/// The era where the offence occurred.
pub(crate) slash_era: EraIndex,
/// The maximum percentage of a slash that ever gets paid out.
/// This is f_inf in the paper.
pub(crate) reward_proportion: Perbill,
}
/// Represents an offence record within the staking system, capturing details about a slashing
/// event.
#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebug)]
pub struct OffenceRecord<AccountId> {
/// The account ID of the entity that reported the offence.
pub reporter: Option<AccountId>,
/// Era at which the offence was reported.
pub reported_era: EraIndex,
/// The specific page of the validator's exposure currently being processed.
///
/// Since a validator's total exposure can span multiple pages, this field serves as a pointer
/// to the current page being evaluated. The processing order starts from the last page
/// and moves backward, decrementing this value with each processed page.
///
/// This ensures that all pages are systematically handled, and it helps track when
/// the entire exposure has been processed.
pub exposure_page: u32,
/// The fraction of the validator's stake to be slashed for this offence.
pub slash_fraction: Perbill,
/// The previous slash fraction of the validator's stake before being updated.
/// If a new, higher slash fraction is reported, this field stores the prior fraction
/// that was overwritten. This helps in tracking changes in slashes across multiple reports for
/// the same era.
pub prior_slash_fraction: Perbill,
}
/// Loads next offence in the processing offence and returns the offense record to be processed.
///
/// Note: this can mutate the following storage
/// - `ProcessingOffence`
/// - `OffenceQueue`
/// - `OffenceQueueEras`
fn next_offence<T: Config>() -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
let maybe_processing_offence = ProcessingOffence::<T>::get();
if let Some((offence_era, offender, offence_record)) = maybe_processing_offence {
// If the exposure page is 0, then the offence has been processed.
if offence_record.exposure_page == 0 {
ProcessingOffence::<T>::kill();
return Some((offence_era, offender, offence_record));
}
// Update the next page.
ProcessingOffence::<T>::put((
offence_era,
&offender,
OffenceRecord {
// decrement the page index.
exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
..offence_record.clone()
},
));
return Some((offence_era, offender, offence_record));
}
// Nothing in processing offence. Try to enqueue the next offence.
let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
let Some(&oldest_era) = eras.first() else { return None };
let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
let next_offence = offence_iter.next();
if let Some((ref validator, ref offence_record)) = next_offence {
// Update the processing offence if the offence is multi-page.
if offence_record.exposure_page > 0 {
// update processing offence with the next page.
ProcessingOffence::<T>::put((
oldest_era,
validator.clone(),
OffenceRecord {
exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
..offence_record.clone()
},
));
}
// Remove from `OffenceQueue`
OffenceQueue::<T>::remove(oldest_era, &validator);
}
// If there are no offences left for the era, remove the era from `OffenceQueueEras`.
if offence_iter.next().is_none() {
if eras.len() == 1 {
// If there is only one era left, remove the entire queue.
OffenceQueueEras::<T>::kill();
} else {
// Remove the oldest era
eras.remove(0);
OffenceQueueEras::<T>::put(eras);
}
}
next_offence.map(|(v, o)| (oldest_era, v, o))
}
/// Infallible function to process an offence.
pub(crate) fn process_offence<T: Config>() -> Weight {
// We do manual weight tracking for early-returns, and use benchmarks for the final two
// branches.
let mut incomplete_consumed_weight = Weight::from_parts(0, 0);
let mut add_db_reads_writes = |reads, writes| {
incomplete_consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
};
add_db_reads_writes(1, 1);
let Some((offence_era, offender, offence_record)) = next_offence::<T>() else {
return incomplete_consumed_weight;
};
log!(
debug,
"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
offender,
offence_era,
offence_record.slash_fraction,
);
add_db_reads_writes(1, 0);
let reward_proportion = SlashRewardFraction::<T>::get();
add_db_reads_writes(2, 0);
let Some(exposure) =
Eras::<T>::get_paged_exposure(offence_era, &offender, offence_record.exposure_page)
else {
// this can only happen if the offence was valid at the time of reporting but became too old
// at the time of computing and should be discarded.
return incomplete_consumed_weight;
};
let slash_page = offence_record.exposure_page;
let slash_defer_duration = T::SlashDeferDuration::get();
let slash_era = offence_era.saturating_add(slash_defer_duration);
add_db_reads_writes(3, 3);
let Some(mut unapplied) = compute_slash::<T>(SlashParams {
stash: &offender,
slash: offence_record.slash_fraction,
prior_slash: offence_record.prior_slash_fraction,
exposure: &exposure,
slash_era: offence_era,
reward_proportion,
}) else {
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
// No slash to apply. Discard.
return incomplete_consumed_weight;
};
<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
offence_era,
slash_era,
offender: offender.clone(),
page: slash_page,
});
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
// add the reporter to the unapplied slash.
unapplied.reporter = offence_record.reporter;
if slash_defer_duration == 0 {
// Apply right away.
log!(
debug,
"🦹 applying slash instantly of {:?} happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
offender,
);
apply_slash::<T>(unapplied, offence_era);
T::WeightInfo::apply_slash().saturating_add(T::WeightInfo::process_offence_queue())
} else {
// Historical Note: Previously, with BondingDuration = 28 and SlashDeferDuration = 27,
// slashes were applied at the start of the 28th era from `offence_era`.
// However, with paged slashing, applying slashes now takes multiple blocks.
// To account for this delay, slashes are now applied at the start of the 27th era from
// `offence_era`.
log!(
debug,
"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
slash_era,
);
UnappliedSlashes::<T>::insert(
slash_era,
(offender, offence_record.slash_fraction, slash_page),
unapplied,
);
T::WeightInfo::process_offence_queue()
}
}
/// Computes a slash of a validator and nominators. It returns an unapplied
/// record to be applied at some later point. Slashing metadata is updated in storage,
/// since unapplied records are only rarely intended to be dropped.
///
/// The pending slash record returned does not have initialized reporters. Those have
/// to be set at a higher level, if any.
///
/// If `nomintors_only` is set to `true`, only the nominator slashes will be computed.
pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> {
let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone());
let mut nominators_slashed = Vec::new();
let (nom_slashed, nom_reward_payout) =
slash_nominators::<T>(params.clone(), &mut nominators_slashed);
reward_payout += nom_reward_payout;
(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
validator: params.stash.clone(),
own: val_slashed,
others: WeakBoundedVec::force_from(
nominators_slashed,
Some("slashed nominators not expected to be larger than the bounds"),
),
reporter: None,
payout: reward_payout,
})
}
/// Compute the slash for a validator. Returns the amount slashed and the reward payout.
fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceOf<T>) {
let own_stake = params.exposure.exposure_metadata.own;
let prior_slashed = params.prior_slash * own_stake;
let new_total_slash = params.slash * own_stake;
let slash_due = new_total_slash.saturating_sub(prior_slashed);
// Audit Note: Previously, each repeated slash reduced the reward by 50% (e.g., 50% × 50% for
// two offences). Since repeat offences in the same era are discarded unless the new slash is
// higher, this reduction logic was unnecessary and removed.
let reward_due = params.reward_proportion * slash_due;
log!(
warn,
"🦹 slashing validator {:?} of stake: {:?} for {:?} in era {:?}",
params.stash,
own_stake,
slash_due,
params.slash_era,
);
(slash_due, reward_due)
}
/// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
///
/// Returns the total amount slashed and amount of reward to pay out.
fn slash_nominators<T: Config>(
params: SlashParams<T>,
nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
) -> (BalanceOf<T>, BalanceOf<T>) {
let mut reward_payout = BalanceOf::<T>::zero();
let mut total_slashed = BalanceOf::<T>::zero();
nominators_slashed.reserve(params.exposure.exposure_page.others.len());
for nominator in &params.exposure.exposure_page.others {
let stash = &nominator.who;
let prior_slashed = params.prior_slash * nominator.value;
let new_slash = params.slash * nominator.value;
// this should always be positive since prior slash is always less than the new slash or
// filtered out when offence is reported (`Pallet::on_new_offences`).
let slash_diff = new_slash.defensive_saturating_sub(prior_slashed);
if slash_diff == Zero::zero() {
// nothing to do
continue;
}
log!(
debug,
"🦹 slashing nominator {:?} of stake: {:?} for {:?} in era {:?}. Prior Slash: {:?}, New Slash: {:?}",
stash,
nominator.value,
slash_diff,
params.slash_era,
params.prior_slash,
params.slash,
);
nominators_slashed.push((stash.clone(), slash_diff));
total_slashed.saturating_accrue(slash_diff);
reward_payout.saturating_accrue(params.reward_proportion * slash_diff);
}
(total_slashed, reward_payout)
}
/// Clear slashing metadata for an obsolete era.
pub(crate) fn clear_era_metadata<T: Config>(obsolete_era: EraIndex) {
#[allow(deprecated)]
ValidatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
}
// apply the slash to a stash account, deducting any missing funds from the reward
// payout, saturating at 0. this is mildly unfair but also an edge-case that
// can only occur when overlapping locked funds have been slashed.
pub fn do_slash<T: Config>(
stash: &T::AccountId,
value: BalanceOf<T>,
reward_payout: &mut BalanceOf<T>,
slashed_imbalance: &mut NegativeImbalanceOf<T>,
slash_era: EraIndex,
) {
let mut ledger =
match Pallet::<T>::ledger(pezsp_staking::StakingAccount::Stash(stash.clone())).defensive() {
Ok(ledger) => ledger,
Err(_) => return, // nothing to do.
};
let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
if value.is_zero() {
// nothing to do
return;
}
// Skip slashing for virtual stakers. The pallets managing them should handle the slashing.
if !Pallet::<T>::is_virtual_staker(stash) {
let (imbalance, missing) = asset::slash::<T>(stash, value);
slashed_imbalance.subsume(imbalance);
if !missing.is_zero() {
// deduct overslash from the reward payout
*reward_payout = reward_payout.saturating_sub(missing);
}
}
let _ = ledger
.update()
.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
// trigger the event
<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
}
/// Apply a previously-unapplied slash.
pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
let mut reward_payout = unapplied_slash.payout;
if unapplied_slash.own > Zero::zero() {
do_slash::<T>(
&unapplied_slash.validator,
unapplied_slash.own,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
for &(ref nominator, nominator_slash) in &unapplied_slash.others {
if nominator_slash.is_zero() {
continue;
}
do_slash::<T>(
nominator,
nominator_slash,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
pay_reporters::<T>(
reward_payout,
slashed_imbalance,
&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
);
}
/// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance.
fn pay_reporters<T: Config>(
reward_payout: BalanceOf<T>,
slashed_imbalance: NegativeImbalanceOf<T>,
reporters: &[T::AccountId],
) {
if reward_payout.is_zero() || reporters.is_empty() {
// nobody to pay out to or nothing to pay;
// just treat the whole value as slashed.
T::Slash::on_unbalanced(slashed_imbalance);
return;
}
// take rewards out of the slashed imbalance.
let reward_payout = reward_payout.min(slashed_imbalance.peek());
let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
for reporter in reporters {
let (reporter_reward, rest) = reward_payout.split(per_reporter);
reward_payout = rest;
// this cancels out the reporter reward imbalance internally, leading
// to no change in total issuance.
asset::deposit_slashed::<T>(reporter, reporter_reward);
}
// the rest goes to the on-slash imbalance handler (e.g. treasury)
value_slashed.subsume(reward_payout); // remainder of reward division remains.
T::Slash::on_unbalanced(value_slashed);
}
@@ -0,0 +1,269 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Testing utils for staking. Provides some common functions to setup staking state, such as
//! bonding validators, nominators, and generating different types of solutions.
use crate::{Pallet as Staking, *};
use pezframe_benchmarking::account;
use pezframe_system::RawOrigin;
use rand_chacha::{
rand_core::{RngCore, SeedableRng},
ChaChaRng,
};
use pezsp_io::hashing::blake2_256;
use pezframe_election_provider_support::SortedListProvider;
use pezframe_support::pezpallet_prelude::*;
use pezsp_runtime::{traits::StaticLookup, Perbill};
const SEED: u32 = 0;
/// This function removes all validators and nominators from storage.
pub fn clear_validators_and_nominators<T: Config>() {
#[allow(deprecated)]
Validators::<T>::remove_all();
// whenever we touch nominators counter we should update `T::VoterList` as well.
#[allow(deprecated)]
Nominators::<T>::remove_all();
// NOTE: safe to call outside block production
T::VoterList::unsafe_clear();
}
/// Grab a funded user.
pub fn create_funded_user<T: Config>(
string: &'static str,
n: u32,
balance_factor: u32,
) -> T::AccountId {
let user = account(string, n, SEED);
let balance = asset::existential_deposit::<T>() * balance_factor.into();
let _ = asset::set_stakeable_balance::<T>(&user, balance);
user
}
/// Grab a funded user with max Balance.
pub fn create_funded_user_with_balance<T: Config>(
string: &'static str,
n: u32,
balance: BalanceOf<T>,
) -> T::AccountId {
let user = account(string, n, SEED);
let _ = asset::set_stakeable_balance::<T>(&user, balance);
user
}
/// Create a stash and controller pair.
pub fn create_stash_controller<T: Config>(
n: u32,
balance_factor: u32,
destination: RewardDestination<T::AccountId>,
) -> Result<(T::AccountId, T::AccountId), &'static str> {
let staker = create_funded_user::<T>("stash", n, balance_factor);
let amount =
asset::existential_deposit::<T>().max(1u64.into()) * (balance_factor / 10).max(1).into();
Staking::<T>::bond(RawOrigin::Signed(staker.clone()).into(), amount, destination)?;
Ok((staker.clone(), staker))
}
/// Create a unique stash and controller pair.
pub fn create_unique_stash_controller<T: Config>(
n: u32,
balance_factor: u32,
destination: RewardDestination<T::AccountId>,
dead_controller: bool,
) -> Result<(T::AccountId, T::AccountId), &'static str> {
let stash = create_funded_user::<T>("stash", n, balance_factor);
let controller = if dead_controller {
create_funded_user::<T>("controller", n, 0)
} else {
create_funded_user::<T>("controller", n, balance_factor)
};
let amount = asset::existential_deposit::<T>() * (balance_factor / 10).max(1).into();
Staking::<T>::bond(RawOrigin::Signed(stash.clone()).into(), amount, destination)?;
// update ledger to be a *different* controller to stash
if let Some(l) = Ledger::<T>::take(&stash) {
<Ledger<T>>::insert(&controller, l);
}
// update bonded account to be unique controller
<Bonded<T>>::insert(&stash, &controller);
Ok((stash, controller))
}
/// Create a stash and controller pair with fixed balance.
pub fn create_stash_controller_with_balance<T: Config>(
n: u32,
balance: crate::BalanceOf<T>,
destination: RewardDestination<T::AccountId>,
) -> Result<(T::AccountId, T::AccountId), &'static str> {
let staker = create_funded_user_with_balance::<T>("stash", n, balance);
Staking::<T>::bond(RawOrigin::Signed(staker.clone()).into(), balance, destination)?;
Ok((staker.clone(), staker))
}
/// Create a stash and controller pair, where payouts go to a dead payee account. This is used to
/// test worst case payout scenarios.
pub fn create_stash_and_dead_payee<T: Config>(
n: u32,
balance_factor: u32,
) -> Result<(T::AccountId, T::AccountId), &'static str> {
let staker = create_funded_user::<T>("stash", n, 0);
// payee has no funds
let payee = create_funded_user::<T>("payee", n, 0);
let amount = asset::existential_deposit::<T>() * (balance_factor / 10).max(1).into();
Staking::<T>::bond(
RawOrigin::Signed(staker.clone()).into(),
amount,
RewardDestination::Account(payee),
)?;
Ok((staker.clone(), staker))
}
/// create `max` validators.
pub fn create_validators<T: Config>(
max: u32,
balance_factor: u32,
) -> Result<Vec<AccountIdLookupOf<T>>, &'static str> {
create_validators_with_seed::<T>(max, balance_factor, 0)
}
/// create `max` validators, with a seed to help unintentional prevent account collisions.
pub fn create_validators_with_seed<T: Config>(
max: u32,
balance_factor: u32,
seed: u32,
) -> Result<Vec<AccountIdLookupOf<T>>, &'static str> {
let mut validators: Vec<AccountIdLookupOf<T>> = Vec::with_capacity(max as usize);
for i in 0..max {
let (stash, controller) =
create_stash_controller::<T>(i + seed, balance_factor, RewardDestination::Staked)?;
let validator_prefs =
ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() };
Staking::<T>::validate(RawOrigin::Signed(controller).into(), validator_prefs)?;
let stash_lookup = T::Lookup::unlookup(stash);
validators.push(stash_lookup);
}
Ok(validators)
}
/// This function generates validators and nominators who are randomly nominating
/// `edge_per_nominator` random validators (until `to_nominate` if provided).
///
/// NOTE: This function will remove any existing validators or nominators to ensure
/// we are working with a clean state.
///
/// Parameters:
/// - `validators`: number of bonded validators
/// - `nominators`: number of bonded nominators.
/// - `edge_per_nominator`: number of edge (vote) per nominator.
/// - `randomize_stake`: whether to randomize the stakes.
/// - `to_nominate`: if `Some(n)`, only the first `n` bonded validator are voted upon. Else, all of
/// them are considered and `edge_per_nominator` random validators are voted for.
///
/// Return the validators chosen to be nominated.
pub fn create_validators_with_nominators_for_era<T: Config>(
validators: u32,
nominators: u32,
edge_per_nominator: usize,
randomize_stake: bool,
to_nominate: Option<u32>,
) -> Result<Vec<AccountIdLookupOf<T>>, &'static str> {
clear_validators_and_nominators::<T>();
let mut validators_stash: Vec<AccountIdLookupOf<T>> = Vec::with_capacity(validators as usize);
let mut rng = ChaChaRng::from_seed(SEED.using_encoded(blake2_256));
// Create validators
for i in 0..validators {
let balance_factor = if randomize_stake { rng.next_u32() % 255 + 10 } else { 100u32 };
let (v_stash, v_controller) =
create_stash_controller::<T>(i, balance_factor, RewardDestination::Staked)?;
let validator_prefs =
ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() };
Staking::<T>::validate(RawOrigin::Signed(v_controller.clone()).into(), validator_prefs)?;
let stash_lookup = T::Lookup::unlookup(v_stash.clone());
validators_stash.push(stash_lookup.clone());
}
let to_nominate = to_nominate.unwrap_or(validators_stash.len() as u32) as usize;
let validator_chosen = validators_stash[0..to_nominate].to_vec();
// Create nominators
for j in 0..nominators {
let balance_factor = if randomize_stake { rng.next_u32() % 255 + 10 } else { 100u32 };
let (_n_stash, n_controller) =
create_stash_controller::<T>(u32::MAX - j, balance_factor, RewardDestination::Staked)?;
// Have them randomly validate
let mut available_validators = validator_chosen.clone();
let mut selected_validators: Vec<AccountIdLookupOf<T>> =
Vec::with_capacity(edge_per_nominator);
for _ in 0..validators.min(edge_per_nominator as u32) {
let selected = rng.next_u32() as usize % available_validators.len();
let validator = available_validators.remove(selected);
selected_validators.push(validator);
if available_validators.is_empty() {
break;
}
}
Staking::<T>::nominate(
RawOrigin::Signed(n_controller.clone()).into(),
selected_validators,
)?;
}
ValidatorCount::<T>::put(validators);
Ok(validator_chosen)
}
/// get the current era.
pub fn current_era<T: Config>() -> EraIndex {
CurrentEra::<T>::get().unwrap_or(0)
}
pub fn migrate_to_old_currency<T: Config>(who: T::AccountId) {
use pezframe_support::traits::LockableCurrency;
let staked = asset::staked::<T>(&who);
// apply locks (this also adds a consumer).
T::OldCurrency::set_lock(
STAKING_ID,
&who,
staked,
pezframe_support::traits::WithdrawReasons::all(),
);
// remove holds.
asset::kill_stake::<T>(&who).expect("remove hold failed");
// replicate old behaviour of explicit increment of consumer.
pezframe_system::Pallet::<T>::inc_consumers(&who).expect("increment consumer failed");
}
/// Set active era to the given era index.
pub fn set_active_era<T: Config>(era: EraIndex) {
// set the current era.
CurrentEra::<T>::put(era);
// set the active era.
ActiveEra::<T>::put(ActiveEraInfo { index: era, start: None });
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
#[test]
fn set_staking_configs_works() {
ExtBuilder::default().build_and_execute(|| {
// setting works
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
ConfigOp::Set(1_500),
ConfigOp::Set(2_000),
ConfigOp::Set(10),
ConfigOp::Set(20),
ConfigOp::Set(Percent::from_percent(75)),
ConfigOp::Set(Zero::zero()),
ConfigOp::Set(Zero::zero())
));
assert_eq!(MinNominatorBond::<Test>::get(), 1_500);
assert_eq!(MinValidatorBond::<Test>::get(), 2_000);
assert_eq!(MaxNominatorsCount::<Test>::get(), Some(10));
assert_eq!(MaxValidatorsCount::<Test>::get(), Some(20));
assert_eq!(ChillThreshold::<Test>::get(), Some(Percent::from_percent(75)));
assert_eq!(MinCommission::<Test>::get(), Perbill::from_percent(0));
assert_eq!(MaxStakedRewards::<Test>::get(), Some(Percent::from_percent(0)));
// noop does nothing
assert_storage_noop!(assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop
)));
// removing works
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove,
ConfigOp::Remove
));
assert_eq!(MinNominatorBond::<Test>::get(), 0);
assert_eq!(MinValidatorBond::<Test>::get(), 0);
assert_eq!(MaxNominatorsCount::<Test>::get(), None);
assert_eq!(MaxValidatorsCount::<Test>::get(), None);
assert_eq!(ChillThreshold::<Test>::get(), None);
assert_eq!(MinCommission::<Test>::get(), Perbill::from_percent(0));
assert_eq!(MaxStakedRewards::<Test>::get(), None);
});
}
@@ -0,0 +1,87 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
#[test]
fn change_controller_works() {
ExtBuilder::default().build_and_execute(|| {
let (stash, controller) = testing_utils::create_unique_stash_controller::<Test>(
0,
100,
RewardDestination::Staked,
false,
)
.unwrap();
// ensure `stash` and `controller` are bonded as stash controller pair.
assert_eq!(Staking::bonded(&stash), Some(controller));
// `controller` can control `stash` who is initially a validator.
assert_ok!(Staking::chill(RuntimeOrigin::signed(controller)));
// sets controller back to `stash`.
assert_ok!(Staking::set_controller(RuntimeOrigin::signed(stash)));
assert_eq!(Staking::bonded(&stash), Some(stash));
// fetch the ledger from storage and check if the controller is correct.
let ledger = Staking::ledger(StakingAccount::Stash(stash)).unwrap();
assert_eq!(ledger.controller(), Some(stash));
// same if we fetch the ledger by controller.
let ledger = Staking::ledger(StakingAccount::Controller(stash)).unwrap();
assert_eq!(ledger.controller, Some(stash));
assert_eq!(ledger.controller(), Some(stash));
// the raw storage ledger's controller is always `None`. however, we can still fetch the
// correct controller with `ledger.controller()`.
let raw_ledger = <Ledger<Test>>::get(&stash).unwrap();
assert_eq!(raw_ledger.controller, None);
// `controller` is no longer in control. `stash` is now controller.
assert_noop!(
Staking::validate(RuntimeOrigin::signed(controller), ValidatorPrefs::default()),
Error::<Test>::NotController,
);
assert_ok!(Staking::validate(RuntimeOrigin::signed(stash), ValidatorPrefs::default()));
})
}
#[test]
fn change_controller_already_paired_once_stash() {
ExtBuilder::default().build_and_execute(|| {
// 11 and 11 are bonded as controller and stash respectively.
assert_eq!(Staking::bonded(&11), Some(11));
// 11 is initially a validator.
assert_ok!(Staking::chill(RuntimeOrigin::signed(11)));
// Controller cannot change once matching with stash.
assert_noop!(
Staking::set_controller(RuntimeOrigin::signed(11)),
Error::<Test>::AlreadyPaired
);
assert_eq!(Staking::bonded(&11), Some(11));
// 10 is no longer in control.
assert_noop!(
Staking::validate(RuntimeOrigin::signed(10), ValidatorPrefs::default()),
Error::<Test>::NotController,
);
assert_ok!(Staking::validate(RuntimeOrigin::signed(11), ValidatorPrefs::default()));
})
}
@@ -0,0 +1,944 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use pezframe_election_provider_support::ElectionDataProvider;
#[test]
fn set_minimum_active_stake_is_correct() {
ExtBuilder::default()
.nominate(false)
.add_staker(61, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.add_staker(71, 10, StakerStatus::<AccountId>::Nominator(vec![21]))
.add_staker(81, 50, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| {
// default bounds are unbounded.
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default(),
0
));
assert_eq!(MinimumActiveStake::<Test>::get(), 10);
// remove staker with lower bond by limiting the number of voters and check
// `MinimumActiveStake` again after electing voters.
let bounds = ElectionBoundsBuilder::default().voters_count(5.into()).build();
assert_ok!(<Staking as ElectionDataProvider>::electing_voters(bounds.voters, 0));
assert_eq!(MinimumActiveStake::<Test>::get(), 50);
});
}
#[test]
fn set_minimum_active_stake_lower_bond_works() {
// lower non-zero active stake below `MinNominatorBond` is the minimum active stake if
// it is selected as part of the npos voters.
ExtBuilder::default().has_stakers(true).nominate(true).build_and_execute(|| {
assert_eq!(MinNominatorBond::<Test>::get(), 1);
assert_eq!(<Test as Config>::VoterList::count(), 4);
assert_ok!(Staking::bond(RuntimeOrigin::signed(4), 5, RewardDestination::Staked,));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), vec![11]));
assert_eq!(<Test as Config>::VoterList::count(), 5);
let voters_before =
<Staking as ElectionDataProvider>::electing_voters(DataProviderBounds::default(), 0)
.unwrap();
assert_eq!(MinimumActiveStake::<Test>::get(), 5);
// update minimum nominator bond.
MinNominatorBond::<Test>::set(10);
assert_eq!(MinNominatorBond::<Test>::get(), 10);
// voter list still considers nominator 4 for voting, even though its active stake is
// lower than `MinNominatorBond`.
assert_eq!(<Test as Config>::VoterList::count(), 5);
let voters =
<Staking as ElectionDataProvider>::electing_voters(DataProviderBounds::default(), 0)
.unwrap();
assert_eq!(voters_before, voters);
// minimum active stake is lower than `MinNominatorBond`.
assert_eq!(MinimumActiveStake::<Test>::get(), 5);
});
}
#[test]
fn set_minimum_active_bond_corrupt_state() {
ExtBuilder::default()
.has_stakers(true)
.nominate(true)
.add_staker(61, 2_000, StakerStatus::<AccountId>::Nominator(vec![21]))
.build_and_execute(|| {
assert_eq!(Staking::weight_of(&101), 500);
let voters = <Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default(),
0,
)
.unwrap();
assert_eq!(voters.len(), 5);
assert_eq!(MinimumActiveStake::<Test>::get(), 500);
Session::roll_until_active_era(10);
assert_ok!(Staking::unbond(RuntimeOrigin::signed(101), 200));
Session::roll_until_active_era(20);
assert_ok!(Staking::unbond(RuntimeOrigin::signed(101), 100));
// corrupt ledger state by lowering max unlocking chunks bounds.
MaxUnlockingChunks::set(1);
let voters = <Staking as ElectionDataProvider>::electing_voters(
DataProviderBounds::default(),
0,
)
.unwrap();
// number of returned voters decreases since ledger entry of stash 101 is now
// corrupt.
assert_eq!(voters.len(), 4);
// minimum active stake does not take into consideration the corrupt entry.
assert_eq!(MinimumActiveStake::<Test>::get(), 2_000);
// voter weight of corrupted ledger entry is 0.
assert_eq!(Staking::weight_of(&101), 0);
// reset max unlocking chunks for try_state to pass.
MaxUnlockingChunks::set(32);
})
}
#[test]
fn voters_include_self_vote() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// default bounds are unbounded.
assert!(<Validators<Test>>::iter().map(|(x, _)| x).all(|v| Staking::electing_voters(
DataProviderBounds::default(),
0
)
.unwrap()
.into_iter()
.any(|(w, _, t)| { v == w && t[0] == w })))
})
}
#[test]
#[should_panic]
#[cfg(debug_assertions)]
fn only_iterates_max_2_times_max_allowed_len() {
ExtBuilder::default()
.nominate(false)
// the best way to invalidate a bunch of nominators is to have them nominate a lot of
// ppl, but then lower the MaxNomination limit.
.add_staker(61, 2_000, StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]))
.add_staker(71, 2_000, StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]))
.add_staker(81, 2_000, StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]))
.build_and_execute(|| {
let bounds_builder = ElectionBoundsBuilder::default();
// all voters ordered by stake,
assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![61, 71, 81, 11, 21, 31]
);
AbsoluteMaxNominations::set(2);
// we want 2 voters now, and in maximum we allow 4 iterations. This is what happens:
// 61 is pruned;
// 71 is pruned;
// 81 is pruned;
// 11 is taken;
// we finish since the 2x limit is reached.
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(2.into()).build().voters, 0)
.unwrap()
.iter()
.map(|(stash, _, _)| stash)
.copied()
.collect::<Vec<_>>(),
vec![11],
);
});
}
#[test]
fn respects_snapshot_count_limits() {
ExtBuilder::default()
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
// sum of all nominators who'd be voters (1), plus the self-votes (4).
assert_eq!(<Test as Config>::VoterList::count(), 5);
let bounds_builder = ElectionBoundsBuilder::default();
// if voter count limit is less..
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(1.into()).build().voters, 0)
.unwrap()
.len(),
1
);
// if voter count limit is equal..
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(5.into()).build().voters, 0)
.unwrap()
.len(),
5
);
// if voter count limit is more.
assert_eq!(
Staking::electing_voters(bounds_builder.voters_count(55.into()).build().voters, 0)
.unwrap()
.len(),
5
);
// if target count limit is more..
assert_eq!(
Staking::electable_targets(
bounds_builder.targets_count(6.into()).build().targets,
0,
)
.unwrap()
.len(),
4
);
// if target count limit is equal..
assert_eq!(
Staking::electable_targets(
bounds_builder.targets_count(4.into()).build().targets,
0,
)
.unwrap()
.len(),
4
);
// if target limit count is less, then we return an error.
assert_eq!(
Staking::electable_targets(
bounds_builder.targets_count(1.into()).build().targets,
0
)
.unwrap()
.len(),
1,
);
});
}
#[test]
fn respects_snapshot_size_limits() {
ExtBuilder::default().build_and_execute(|| {
// voters: set size bounds that allows only for 1 voter.
let bounds = ElectionBoundsBuilder::default().voters_size(26.into()).build();
let elected = Staking::electing_voters(bounds.voters, 0).unwrap();
assert!(elected.encoded_size() == 26 as usize);
let prev_len = elected.len();
// larger size bounds means more quota for voters.
let bounds = ElectionBoundsBuilder::default().voters_size(100.into()).build();
let elected = Staking::electing_voters(bounds.voters, 0).unwrap();
assert!(elected.encoded_size() <= 100 as usize);
assert!(elected.len() > 1 && elected.len() > prev_len);
// targets: set size bounds that allows for only one target to fit in the snapshot.
let bounds = ElectionBoundsBuilder::default().targets_size(10.into()).build();
let elected = Staking::electable_targets(bounds.targets, 0).unwrap();
assert!(elected.encoded_size() == 9 as usize);
let prev_len = elected.len();
// larger size bounds means more space for targets.
let bounds = ElectionBoundsBuilder::default().targets_size(100.into()).build();
let elected = Staking::electable_targets(bounds.targets, 0).unwrap();
assert!(elected.encoded_size() <= 100 as usize);
assert!(elected.len() > 1 && elected.len() > prev_len);
});
}
#[test]
fn nomination_quota_checks_at_nominate_works() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// stash bond of 222 has a nomination quota of 2 targets.
bond(61, 222);
assert_eq!(Staking::api_nominations_quota(222), 2);
// nominating with targets below the nomination quota works.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(61), vec![11]));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(61), vec![11, 21]));
// nominating with targets above the nomination quota returns error.
assert_noop!(
Staking::nominate(RuntimeOrigin::signed(61), vec![11, 21, 31]),
Error::<Test>::TooManyTargets
);
});
}
#[test]
#[should_panic]
#[cfg(debug_assertions)]
fn change_of_absolute_max_nominations() {
use pezframe_election_provider_support::ElectionDataProvider;
ExtBuilder::default()
.add_staker(61, 10, StakerStatus::Nominator(vec![1]))
.add_staker(71, 10, StakerStatus::Nominator(vec![1, 2, 3]))
.balance_factor(10)
.build_and_execute(|| {
// pre-condition
assert_eq!(AbsoluteMaxNominations::get(), 16);
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)]
);
// default bounds are unbounded.
let bounds = DataProviderBounds::default();
// 3 validators and 3 nominators
assert_eq!(Staking::electing_voters(bounds, 0).unwrap().len(), 3 + 3);
// abrupt change from 16 to 4, everyone should be fine.
AbsoluteMaxNominations::set(4);
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)]
);
assert_eq!(Staking::electing_voters(bounds, 0).unwrap().len(), 3 + 3);
// No one can be chilled on account of non-decodable keys.
for k in Nominators::<Test>::iter_keys() {
assert_noop!(
Staking::chill_other(RuntimeOrigin::signed(1), k),
Error::<Test>::CannotChillOther
);
}
// abrupt change from 4 to 3, everyone should be fine.
AbsoluteMaxNominations::set(3);
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(101, 2), (71, 3), (61, 1)]
);
assert_eq!(Staking::electing_voters(bounds, 0).unwrap().len(), 3 + 3);
// As before, no one can be chilled on account of non-decodable keys.
for k in Nominators::<Test>::iter_keys() {
assert_noop!(
Staking::chill_other(RuntimeOrigin::signed(1), k),
Error::<Test>::CannotChillOther
);
}
// abrupt change from 3 to 2, this should cause some nominators to be non-decodable,
// and thus non-existent unless they update.
AbsoluteMaxNominations::set(2);
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(101, 2), (61, 1)]
);
// 101 and 61 still cannot be chilled by someone else.
for k in [101, 61].iter() {
assert_noop!(
Staking::chill_other(RuntimeOrigin::signed(1), *k),
Error::<Test>::CannotChillOther
);
}
// 71 is still in storage..
assert!(Nominators::<Test>::contains_key(71));
// but its value cannot be decoded and default is returned.
assert!(Nominators::<Test>::get(71).is_none());
assert_eq!(Staking::electing_voters(bounds, 0).unwrap().len(), 3 + 2);
assert!(Nominators::<Test>::contains_key(101));
// abrupt change from 2 to 1, this should cause some nominators to be non-decodable,
// and thus non-existent unless they update.
AbsoluteMaxNominations::set(1);
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(61, 1)]
);
// 61 *still* cannot be chilled by someone else.
assert_noop!(
Staking::chill_other(RuntimeOrigin::signed(1), 61),
Error::<Test>::CannotChillOther
);
assert!(Nominators::<Test>::contains_key(71));
assert!(Nominators::<Test>::contains_key(61));
assert!(Nominators::<Test>::get(71).is_none());
assert!(Nominators::<Test>::get(61).is_some());
assert_eq!(Staking::electing_voters(bounds, 0).unwrap().len(), 3 + 1);
// now one of them can revive themselves by re-nominating to a proper value.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(71), vec![1]));
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(71, 1), (61, 1)]
);
// or they can be chilled by any account.
assert!(Nominators::<Test>::contains_key(101));
assert!(Nominators::<Test>::get(101).is_none());
assert_ok!(Staking::chill_other(RuntimeOrigin::signed(71), 101));
assert_eq!(*staking_events().last().unwrap(), Event::Chilled { stash: 101 });
assert!(!Nominators::<Test>::contains_key(101));
assert!(Nominators::<Test>::get(101).is_none());
})
}
#[test]
fn nomination_quota_max_changes_decoding() {
use pezframe_election_provider_support::ElectionDataProvider;
ExtBuilder::default()
.nominate(false)
.set_status(41, StakerStatus::Validator)
.add_staker(60, 10, StakerStatus::Nominator(vec![11]))
.add_staker(70, 10, StakerStatus::Nominator(vec![11, 21, 31]))
.add_staker(30, 10, StakerStatus::Nominator(vec![11, 21, 31, 41]))
.add_staker(50, 10, StakerStatus::Nominator(vec![11, 21, 31, 41]))
.balance_factor(11)
.build_and_execute(|| {
// pre-condition.
assert_eq!(MaxNominationsOf::<Test>::get(), 16);
let unbounded_election = DataProviderBounds::default();
assert_eq!(
Nominators::<Test>::iter()
.map(|(k, n)| (k, n.targets.len()))
.collect::<Vec<_>>(),
vec![(70, 3), (50, 4), (30, 4), (60, 1)]
);
// 4 validators and 4 nominators
assert_eq!(Staking::electing_voters(unbounded_election, 0).unwrap().len(), 4 + 4);
});
}
#[test]
fn api_nominations_quota_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(Staking::api_nominations_quota(10), MaxNominationsOf::<Test>::get());
assert_eq!(Staking::api_nominations_quota(333), MaxNominationsOf::<Test>::get());
assert_eq!(Staking::api_nominations_quota(222), 2);
assert_eq!(Staking::api_nominations_quota(111), 1);
})
}
#[test]
fn lazy_quota_npos_voters_works_above_quota() {
ExtBuilder::default()
.nominate(false)
// need to make 22, 23, 24 and 25 validators
.add_staker(22, 1000, StakerStatus::Validator)
.add_staker(23, 1000, StakerStatus::Validator)
.add_staker(24, 1000, StakerStatus::Validator)
.add_staker(25, 1000, StakerStatus::Validator)
.add_staker(
61,
300, // 300 bond has 16 nomination quota.
StakerStatus::<AccountId>::Nominator(vec![21, 22, 23, 24, 25]),
)
.build_and_execute(|| {
// unbond 78 from stash 60 so that it's bonded balance is 222, which has a lower
// nomination quota than at nomination time (max 2 targets).
assert_ok!(Staking::unbond(RuntimeOrigin::signed(61), 78));
assert_eq!(Staking::api_nominations_quota(300 - 78), 2);
// even through 61 has nomination quota of 2 at the time of the election, all the
// nominations (5) will be used.
assert_eq!(
Staking::electing_voters(DataProviderBounds::default(), 0)
.unwrap()
.iter()
.map(|(stash, _, targets)| (*stash, targets.len()))
.collect::<Vec<_>>(),
vec![(11, 1), (21, 1), (31, 1), (22, 1), (23, 1), (24, 1), (25, 1), (61, 5)],
);
});
}
#[test]
fn nominations_quota_limits_size_work() {
ExtBuilder::default()
.nominate(false)
.set_status(41, StakerStatus::Validator)
.add_staker(71, 333, StakerStatus::<AccountId>::Nominator(vec![11, 21, 31, 41]))
.build_and_execute(|| {
// nominations of 71 won't be added due to voter size limit exceeded.
let bounds = ElectionBoundsBuilder::default().voters_size(101.into()).build();
assert_eq!(
Staking::electing_voters(bounds.voters, 0)
.unwrap()
.iter()
.map(|(stash, _, targets)| (*stash, targets.len()))
.collect::<Vec<_>>(),
vec![(41, 1), (11, 1), (21, 1), (31, 1)],
);
assert_eq!(
*staking_events().last().unwrap(),
Event::SnapshotVotersSizeExceeded { size: 100 }
);
// however, if the election voter size bounds were larger, the snapshot would
// include the electing voters of 70.
let bounds = ElectionBoundsBuilder::default().voters_size(1_000.into()).build();
assert_eq!(
Staking::electing_voters(bounds.voters, 0)
.unwrap()
.iter()
.map(|(stash, _, targets)| (*stash, targets.len()))
.collect::<Vec<_>>(),
vec![(41, 1), (11, 1), (21, 1), (31, 1), (71, 4)],
);
});
}
mod sorted_list_provider {
use super::*;
use pezframe_election_provider_support::SortedListProvider;
#[test]
fn re_nominate_does_not_change_counters_or_list() {
ExtBuilder::default().nominate(true).build_and_execute(|| {
// given
let pre_insert_voter_count =
(Nominators::<Test>::count() + Validators::<Test>::count()) as u32;
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![11, 21, 31, 101]
);
// when account 101 renominates
assert_ok!(Staking::nominate(RuntimeOrigin::signed(101), vec![31]));
// then counts don't change
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
// and the list is the same
assert_eq!(
<Test as Config>::VoterList::iter().collect::<Vec<_>>(),
vec![11, 21, 31, 101]
);
});
}
#[test]
fn re_validate_does_not_change_counters_or_list() {
ExtBuilder::default().nominate(false).build_and_execute(|| {
// given
let pre_insert_voter_count =
(Nominators::<Test>::count() + Validators::<Test>::count()) as u32;
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
assert_eq!(<Test as Config>::VoterList::iter().collect::<Vec<_>>(), vec![11, 21, 31]);
// when account 11 re-validates
assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default()));
// then counts don't change
assert_eq!(<Test as Config>::VoterList::count(), pre_insert_voter_count);
// and the list is the same
assert_eq!(<Test as Config>::VoterList::iter().collect::<Vec<_>>(), vec![11, 21, 31]);
});
}
}
mod paged_snapshot {
use super::*;
#[test]
fn target_snapshot_works() {
ExtBuilder::default()
.nominate(true)
.set_status(41, StakerStatus::Validator)
.set_status(51, StakerStatus::Validator)
.set_status(101, StakerStatus::Idle)
.build_and_execute(|| {
// all registered validators.
let all_targets = vec![51, 31, 41, 21, 11];
assert_eq_uvec!(
<Test as Config>::TargetList::iter().collect::<Vec<_>>(),
all_targets,
);
// 3 targets per page.
let bounds =
ElectionBoundsBuilder::default().targets_count(3.into()).build().targets;
let targets =
<Staking as ElectionDataProvider>::electable_targets(bounds, 0).unwrap();
assert_eq_uvec!(targets, all_targets.iter().take(3).cloned().collect::<Vec<_>>());
// emulates a no bounds target snapshot request.
let bounds =
ElectionBoundsBuilder::default().targets_count(u32::MAX.into()).build().targets;
let single_page_targets =
<Staking as ElectionDataProvider>::electable_targets(bounds, 0).unwrap();
// complete set of paged targets is the same as single page, no bounds set of
// targets.
assert_eq_uvec!(all_targets, single_page_targets);
})
}
#[test]
fn target_snapshot_multi_page_redundant() {
ExtBuilder::default().build_and_execute(|| {
let all_targets = vec![31, 21, 11];
assert_eq_uvec!(<Test as Config>::TargetList::iter().collect::<Vec<_>>(), all_targets,);
// no bounds.
let bounds =
ElectionBoundsBuilder::default().targets_count(u32::MAX.into()).build().targets;
// target snapshot supports only single-page, thus it is redundant what's the page index
// requested.
let snapshot = Staking::electable_targets(bounds, 0).unwrap();
assert!(
snapshot == all_targets &&
snapshot == Staking::electable_targets(bounds, 1).unwrap() &&
snapshot == Staking::electable_targets(bounds, 2).unwrap() &&
snapshot == Staking::electable_targets(bounds, u32::MAX).unwrap(),
);
})
}
#[test]
fn voter_snapshot_works() {
ExtBuilder::default()
.nominate(true)
.set_status(51, StakerStatus::Validator)
.set_status(41, StakerStatus::Nominator(vec![51]))
.set_status(101, StakerStatus::Validator)
.build_and_execute(|| {
let bounds = ElectionBoundsBuilder::default().voters_count(3.into()).build().voters;
assert_eq!(
<Test as Config>::VoterList::iter()
.collect::<Vec<_>>()
.into_iter()
.map(|v| (v, <Test as Config>::VoterList::get_score(&v).unwrap()))
.collect::<Vec<_>>(),
vec![(51, 5000), (41, 4000), (11, 1000), (21, 1000), (31, 500), (101, 500)],
);
let mut all_voters = vec![];
let voters_page_3 = <Staking as ElectionDataProvider>::electing_voters(bounds, 3)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
all_voters.extend(voters_page_3.clone());
assert_eq!(voters_page_3, vec![51, 41, 11]);
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Ongoing(11));
let voters_page_2 = <Staking as ElectionDataProvider>::electing_voters(bounds, 2)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
all_voters.extend(voters_page_2.clone());
assert_eq!(voters_page_2, vec![21, 31, 101]);
// all voters in the list have been consumed.
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Consumed);
// thus page 1 and 0 are empty.
assert!(<Staking as ElectionDataProvider>::electing_voters(bounds, 1)
.unwrap()
.is_empty());
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Consumed);
assert!(<Staking as ElectionDataProvider>::electing_voters(bounds, 0)
.unwrap()
.is_empty());
// last page has been requested, reset the snapshot status to waiting.
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Waiting);
// now request 1 page with bounds where all registered voters fit. u32::MAX
// emulates a no bounds request.
let bounds =
ElectionBoundsBuilder::default().voters_count(u32::MAX.into()).build().targets;
let single_page_voters =
<Staking as ElectionDataProvider>::electing_voters(bounds, 0)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
// complete set of paged voters is the same as single page, no bounds set of
// voters.
assert_eq!(all_voters, single_page_voters);
})
}
#[test]
fn voter_list_locked_during_multi_page_snapshot() {
ExtBuilder::default()
.nominate(true)
.set_status(51, StakerStatus::Validator)
.set_status(41, StakerStatus::Nominator(vec![51]))
.set_status(101, StakerStatus::Validator)
.build_and_execute(|| {
let bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build().voters;
assert_eq!(
<Test as Config>::VoterList::iter()
.collect::<Vec<_>>()
.into_iter()
.map(|v| (v, <Test as Config>::VoterList::get_score(&v).unwrap()))
.collect::<Vec<_>>(),
vec![(51, 5000), (41, 4000), (11, 1000), (21, 1000), (31, 500), (101, 500)],
);
// initially not locked
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), None);
let voters_page_3 = <Staking as ElectionDataProvider>::electing_voters(bounds, 3)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
assert_eq!(voters_page_3, vec![51, 41]);
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Ongoing(41));
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), Some(()));
hypothetically!({});
let voters_page_2 = <Staking as ElectionDataProvider>::electing_voters(bounds, 2)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
// still locked
assert_eq!(voters_page_2, vec![11, 21]);
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Ongoing(21));
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), Some(()));
let voters_page_1 = <Staking as ElectionDataProvider>::electing_voters(bounds, 1)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
// consumed, and we already unlock
assert_eq_uvec!(voters_page_1, vec![31, 101]);
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Consumed);
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), None);
// calling page zero will unlock us.
assert!(<Staking as ElectionDataProvider>::electing_voters(bounds, 0)
.unwrap()
.is_empty());
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Waiting);
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), None);
})
}
#[test]
fn voter_list_not_updated_when_locked() {
ExtBuilder::default()
.nominate(true)
.set_status(51, StakerStatus::Validator)
.set_status(41, StakerStatus::Nominator(vec![51]))
.set_status(101, StakerStatus::Validator)
.build_and_execute(|| {
let bounds = ElectionBoundsBuilder::default().voters_count(2.into()).build().voters;
assert_eq!(
<Test as Config>::VoterList::iter()
.collect::<Vec<_>>()
.into_iter()
.map(|v| (v, <Test as Config>::VoterList::get_score(&v).unwrap()))
.collect::<Vec<_>>(),
vec![(51, 5000), (41, 4000), (11, 1000), (21, 1000), (31, 500), (101, 500)],
);
// initial bag of 51
assert_eq!(
pezpallet_bags_list::ListNodes::<T, VoterBagsListInstance>::get(51)
.unwrap()
.bag_upper,
10_000
);
// original bag of 11
assert_eq!(
pezpallet_bags_list::ListNodes::<T, VoterBagsListInstance>::get(11)
.unwrap()
.bag_upper,
1000
);
// initially not locked
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), None);
let voters_page_3 = <Staking as ElectionDataProvider>::electing_voters(bounds, 3)
.unwrap()
.into_iter()
.map(|(a, _, _)| a)
.collect::<Vec<_>>();
assert_eq!(voters_page_3, vec![51, 41]);
assert_eq!(VoterSnapshotStatus::<Test>::get(), SnapshotStatus::Ongoing(41));
assert_eq!(pezpallet_bags_list::Lock::<T, VoterBagsListInstance>::get(), Some(()));
// 51 who is already part of the list might want to unbond. They are already in the
// snapshot, and their position is not updated
hypothetically!({
assert_ok!(Staking::unbond(RuntimeOrigin::signed(51), 500));
// they are still in the original bag
assert_eq!(
pezpallet_bags_list::ListNodes::<T, VoterBagsListInstance>::get(51)
.unwrap()
.bag_upper,
10_000
);
});
// 11 who is not part of the snapshot yet might want to bond a lot extra, this is
// not reflected in this election.
hypothetically!({
crate::asset::set_stakeable_balance::<T>(&11, 10000);
assert_ok!(Staking::bond_extra(RuntimeOrigin::signed(11), 5000));
// they are still in the original bag
assert_eq!(
pezpallet_bags_list::ListNodes::<T, VoterBagsListInstance>::get(11)
.unwrap()
.bag_upper,
1000
);
});
})
}
}
mod score_provider {
use pezframe_election_provider_support::ScoreProvider;
use super::*;
#[test]
fn no_score_for_chilled_stakers() {
ExtBuilder::default().build_and_execute(|| {
// given 41 being a chilled staker
assert!(
Ledger::<Test>::get(41).is_some() &&
!Validators::<Test>::contains_key(41) &&
!Nominators::<Test>::contains_key(41)
);
// then they will not have a score when bags-list wants to update it.
assert!(<Staking as ScoreProvider<_>>::score(&41).is_none());
});
}
#[test]
fn no_score_for_non_stakers() {
ExtBuilder::default().build_and_execute(|| {
// given 777 being neither a nominator nor a validator in this pallet.
assert!(
!Ledger::<Test>::get(777).is_some() &&
!Validators::<Test>::contains_key(777) &&
!Nominators::<Test>::contains_key(777)
);
// then it will not have a score when bags-list wants to update it.
assert!(<Staking as ScoreProvider<_>>::score(&777).is_none());
});
}
#[test]
fn score_for_validators_nominators() {
ExtBuilder::default().nominate(true).build_and_execute(|| {
// Given 101 being a nominator
assert!(
Ledger::<Test>::get(101).unwrap().active == 500 &&
!Validators::<Test>::contains_key(101) &&
Nominators::<Test>::contains_key(101)
);
// then it will have a score.
assert_eq!(<Staking as ScoreProvider<_>>::score(&101), Some(500));
// given 11 being a validator
assert!(
Ledger::<Test>::get(11).unwrap().active == 1000 &&
Validators::<Test>::contains_key(11) &&
!Nominators::<Test>::contains_key(11)
);
// then it will have a score.
assert_eq!(<Staking as ScoreProvider<_>>::score(&11), Some(1000));
});
}
}
#[test]
fn from_most_staked_to_least_staked() {
ExtBuilder::default()
.nominate(true)
.set_status(51, StakerStatus::Validator)
.set_status(41, StakerStatus::Nominator(vec![51]))
.set_status(101, StakerStatus::Validator)
.set_stake(41, 11000)
.set_stake(51, 2500)
.set_stake(101, 35)
.build_and_execute(|| {
assert_eq!(THRESHOLDS.to_vec(), [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]);
assert_eq!(
<Test as Config>::VoterList::iter()
.collect::<Vec<_>>()
.into_iter()
.map(|v| (v, <Test as Config>::VoterList::get_score(&v).unwrap()))
.collect::<Vec<_>>(),
vec![(41, 11000), (51, 2500), (11, 1000), (21, 1000), (31, 500), (101, 35)],
);
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,607 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{
session_rotation::{Eras, Rotator},
tests::session_mock::{CurrentIndex, Timestamp},
};
use super::*;
#[test]
fn forcing_force_none() {
ExtBuilder::default().build_and_execute(|| {
ForceEra::<T>::put(Forcing::ForceNone);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 6, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 7, active_era: 1, planned_era: 1 }]
);
Session::roll_to_next_session();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SessionRotated { starting_session: 8, active_era: 1, planned_era: 1 }]
);
});
}
#[test]
fn forcing_no_forcing_default() {
ExtBuilder::default().build_and_execute(|| {
// default value, setting it again just for read-ability.
ForceEra::<T>::put(Forcing::NotForcing);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 7500 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
});
}
#[test]
fn forcing_force_always() {
ExtBuilder::default()
.session_per_era(6)
.no_flush_events()
.build_and_execute(|| {
// initial events thus far, without `ForceAlways` set.
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 1, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 2, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 3, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 4, active_era: 0, planned_era: 1 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 0, planned_era: 1 },
Event::EraPaid { era_index: 0, validator_payout: 15000, remainder: 15000 },
Event::SessionRotated { starting_session: 6, active_era: 1, planned_era: 1 }
]
);
// but with it set..
ForceEra::<T>::put(Forcing::ForceAlways);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
// we immediately plan a new era as soon as the first session report comes in
Event::SessionRotated { starting_session: 7, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
// by now it is given to mock session, and is buffered
Event::SessionRotated { starting_session: 8, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 7500 },
// and by now it is activated. Note how the validator payout is less, since the
// era duration is less. Note that we immediately plan the next era as well.
Event::SessionRotated { starting_session: 9, active_era: 2, planned_era: 3 }
]
);
});
}
#[test]
fn forcing_force_new() {
ExtBuilder::default()
.session_per_era(6)
.no_flush_events()
.build_and_execute(|| {
// initial events thus far, without `ForceAlways` set.
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 1, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 2, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 3, active_era: 0, planned_era: 0 },
Event::SessionRotated { starting_session: 4, active_era: 0, planned_era: 1 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 0, planned_era: 1 },
Event::EraPaid { era_index: 0, validator_payout: 15000, remainder: 15000 },
Event::SessionRotated { starting_session: 6, active_era: 1, planned_era: 1 }
]
);
// but with it set..
ForceEra::<T>::put(Forcing::ForceNew);
// one era happens quicker
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
// we immediately plan a new era as soon as the first session report comes in
Event::SessionRotated { starting_session: 7, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
// by now it is given to mock session, and is buffered
Event::SessionRotated { starting_session: 8, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 7500 },
// and by now it is activated. Note how the validator payout is less, since the
// era duration is less.
Event::SessionRotated { starting_session: 9, active_era: 2, planned_era: 2 }
]
);
// And the next era goes back to normal.
Session::roll_until_active_era(3);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 10, active_era: 2, planned_era: 2 },
Event::SessionRotated { starting_session: 11, active_era: 2, planned_era: 2 },
Event::SessionRotated { starting_session: 12, active_era: 2, planned_era: 2 },
Event::SessionRotated { starting_session: 13, active_era: 2, planned_era: 3 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 14, active_era: 2, planned_era: 3 },
Event::EraPaid { era_index: 2, validator_payout: 15000, remainder: 15000 },
Event::SessionRotated { starting_session: 15, active_era: 3, planned_era: 3 }
]
);
});
}
#[test]
fn activation_timestamp_when_no_planned_era() {
// maybe not needed, as we have the id check
ExtBuilder::default().session_per_era(6).build_and_execute(|| {
Session::roll_until_active_era(2);
let current_index = CurrentIndex::get();
// reset events until now.
let _ = staking_events_since_last_call();
// GIVEN: no new planned era
assert_eq!(Rotator::<T>::active_era(), 2);
assert_eq!(Rotator::<T>::planned_era(), 2);
// WHEN: send a new activation timestamp (manually).
<Staking as pezpallet_staking_async_rc_client::AHStakingInterface>::on_relay_session_report(
pezpallet_staking_async_rc_client::SessionReport::new_terminal(
current_index,
vec![],
// sending a timestamp that is in the future with identifier of the next era that
// is not planned.
Some((Timestamp::get() + time_per_session(), 3)),
),
);
// THEN: No era rotation should happen, but an error event should be emitted.
assert_eq!(
staking_events_since_last_call(),
vec![
Event::Unexpected(UnexpectedKind::UnknownValidatorActivation),
Event::SessionRotated {
starting_session: current_index + 1,
active_era: 2,
planned_era: 2
}
]
);
});
}
#[test]
#[should_panic]
fn activation_timestamp_when_era_planning_not_complete() {
// maybe not needed, as we have the id check
todo!("what if we receive an activation timestamp when the era planning (election) is not complete?");
}
#[test]
fn max_era_duration_safety_guard() {
ExtBuilder::default().build_and_execute(|| {
// let's deduce some magic numbers for the test.
let ideal_era_payout = total_payout_for(time_per_era());
let ideal_treasury_payout = RemainderRatio::get() * ideal_era_payout;
let ideal_validator_payout = ideal_era_payout - ideal_treasury_payout;
// max era duration is capped to 7 times the ideal era duration.
let max_validator_payout = 7 * ideal_validator_payout;
let max_treasury_payout = 7 * ideal_treasury_payout;
// these are the values we expect to see in the events.
assert_eq!(ideal_treasury_payout, 7500);
assert_eq!(ideal_validator_payout, 7500);
// when the era duration exceeds `MaxEraDuration`, the payouts should be capped to the
// following values.
assert_eq!(max_treasury_payout, 52500);
assert_eq!(max_validator_payout, 52500);
// GIVEN: we are at end of an era (2).
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid {
era_index: 1,
validator_payout: ideal_validator_payout,
remainder: ideal_treasury_payout
},
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
// WHEN: subsequent era takes longer than MaxEraDuration.
// (this can happen either because of a bug or because a long stall in the chain).
Timestamp::set(Timestamp::get() + 2 * MaxEraDuration::get());
Session::roll_until_active_era(3);
// THEN: we should see the payouts capped to the max values.
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 7, active_era: 2, planned_era: 3 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 8, active_era: 2, planned_era: 3 },
// an event is emitted to indicate something unexpected happened, i.e. the era
// duration exceeded the `MaxEraDuration` limit.
Event::Unexpected(UnexpectedKind::EraDurationBoundExceeded),
// the payouts are capped to the max values.
Event::EraPaid {
era_index: 2,
validator_payout: max_validator_payout,
remainder: max_treasury_payout
},
Event::SessionRotated { starting_session: 9, active_era: 3, planned_era: 3 }
]
);
});
}
#[test]
fn era_cleanup_history_depth_works_with_prune_era_step_extrinsic() {
ExtBuilder::default().build_and_execute(|| {
// Test that era pruning does not happen automatically
assert_eq!(active_era(), 1);
Session::roll_until_active_era(HistoryDepth::get() - 1);
assert!(matches!(
&staking_events_since_last_call()[..],
&[
..,
Event::SessionRotated { starting_session: 236, active_era: 78, planned_era: 79 },
Event::EraPaid { era_index: 78, validator_payout: 7500, remainder: 7500 },
Event::SessionRotated { starting_session: 237, active_era: 79, planned_era: 79 }
]
));
// All eras from 1 to current still present
assert_ok!(Eras::<T>::era_fully_present(1));
assert_ok!(Eras::<T>::era_fully_present(2));
// ..
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get() - 1));
Session::roll_until_active_era(HistoryDepth::get());
assert_ok!(Eras::<T>::era_fully_present(1));
assert_ok!(Eras::<T>::era_fully_present(2));
// ..
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get()));
// Eras should NOT be automatically pruned
Session::roll_until_active_era(HistoryDepth::get() + 1);
assert_ok!(Eras::<T>::era_fully_present(1));
assert_ok!(Eras::<T>::era_fully_present(2));
// ..
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get() + 1));
assert!(matches!(
&staking_events_since_last_call()[..],
&[
..,
Event::EraPaid { era_index: 80, validator_payout: 7500, remainder: 7500 },
// NO EraPruned event - pruning is now manual
Event::SessionRotated { starting_session: 243, active_era: 81, planned_era: 81 }
]
));
// Roll forward more, era 1 is now prunable
Session::roll_until_active_era(HistoryDepth::get() + 2);
assert_ok!(Eras::<T>::era_fully_present(1)); // Era 1 still exists!
assert_ok!(Eras::<T>::era_fully_present(2));
assert_ok!(Eras::<T>::era_fully_present(3));
// ..
assert_ok!(Eras::<T>::era_fully_present(HistoryDepth::get() + 2));
assert!(matches!(
&staking_events_since_last_call()[..],
&[
..,
Event::EraPaid { era_index: 81, validator_payout: 7500, remainder: 7500 },
// NO EraPruned event - pruning is now manual
Event::SessionRotated { starting_session: 246, active_era: 82, planned_era: 82 }
]
));
// Only old eras (outside pruning window) can be pruned
// Try to prune era 2 (should fail as it's within the history window)
assert_noop!(
Staking::prune_era_step(RuntimeOrigin::signed(99), 2),
Error::<T>::EraNotPrunable
);
// Try to prune the current era
assert_noop!(
Staking::prune_era_step(RuntimeOrigin::signed(99), HistoryDepth::get() + 2),
Error::<T>::EraNotPrunable
);
// Verify that we can manually prune era 1 (which is outside history window) and check that
// we progress through all PruningStep states in the exact order, with storage cleanup
// verification
use crate::PruningStep::*;
// Process each pruning step in the exact order defined by the implementation
// Each step should clean its specific storage and transition to the next step
// Process each pruning step, potentially with multiple calls due to item limits
let steps_order = [
ErasStakersPaged,
ErasStakersOverview,
ErasValidatorPrefs,
ClaimedRewards,
ErasValidatorReward,
ErasRewardPoints,
ErasTotalStake,
];
let _ = staking_events_since_last_call();
for expected_step in steps_order.iter() {
// May need multiple calls for steps with lots of data due to weight limits
loop {
let current_state = EraPruningState::<T>::get(1)
.expect("Era 1 should be marked for pruning at this point");
assert_eq!(
current_state, *expected_step,
"Expected to be in step {:?} but was in {:?}",
expected_step, current_state
);
let result = Staking::prune_era_step(RuntimeOrigin::signed(99), 1);
assert_ok!(&result);
let post_info = result.unwrap();
// When work is actually done (pruning storage), should return Pays::No
assert_eq!(
post_info.pays_fee,
pezframe_support::dispatch::Pays::No,
"Should return Pays::No when work is done for step {:?}",
expected_step
);
// Verify weight tracking and limits
assert!(
post_info.actual_weight.is_some(),
"Should report actual weight for {:?}",
expected_step
);
let actual_weight = post_info.actual_weight.unwrap();
assert!(
actual_weight.ref_time() > 0,
"Should report non-zero ref_time for {:?}",
expected_step
);
// No need to validate against limits since we use item-based limiting
// Check if we've moved to the next step (step completed)
let new_state = EraPruningState::<T>::get(1).unwrap_or(ErasStakersPaged);
if new_state != current_state {
break; // Step completed, move to next
}
// Otherwise continue with same step (partial completion due to item limits)
}
// Verify the specific storage is cleaned after completing this step
match expected_step {
ErasStakersPaged => assert_eq!(
crate::ErasStakersPaged::<T>::iter_prefix_values((1,)).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasStakersOverview => assert_eq!(
crate::ErasStakersOverview::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasValidatorPrefs => assert_eq!(
crate::ErasValidatorPrefs::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ClaimedRewards => assert_eq!(
crate::ClaimedRewards::<T>::iter_prefix_values(1).count(),
0,
"{expected_step:?} should be empty after completing step"
),
ErasValidatorReward => assert!(
!crate::ErasValidatorReward::<T>::contains_key(1),
"{expected_step:?} should be empty after completing step"
),
ErasRewardPoints => assert!(
!crate::ErasRewardPoints::<T>::contains_key(1),
"{expected_step:?} should be empty after completing step"
),
ErasTotalStake => assert!(
!crate::ErasTotalStake::<T>::contains_key(1),
"{expected_step:?} should be empty after completing step"
),
}
}
// After final step (ErasTotalStake), the EraPruningState should be removed
assert!(
EraPruningState::<T>::get(1).is_none(),
"EraPruningState should be removed after final step"
);
// Should emit exactly one EraPruned event when manual pruning completes
assert!(matches!(&staking_events_since_last_call()[..], &[Event::EraPruned { index: 1 }]));
// Attempting to prune again should return an error
let result = Staking::prune_era_step(RuntimeOrigin::signed(99), 1);
assert_noop!(result, Error::<T>::EraNotPrunable);
// Now era 1 should be absent
assert_ok!(Eras::<T>::era_absent(1));
// But era 2 should still be present (not automatically pruned)
assert_ok!(Eras::<T>::era_fully_present(2));
// Call the extrinsic on an already pruned era (should return error)
let result = Staking::prune_era_step(RuntimeOrigin::signed(99), 1);
assert_noop!(result, Error::<T>::EraNotPrunable);
});
}
#[test]
fn progress_many_eras_with_try_state() {
// a bit slow, but worthwhile
ExtBuilder::default().build_and_execute(|| {
Session::roll_until_active_era_with(
HistoryDepth::get().max(BondingDuration::get()) + 2,
|| {
Staking::do_try_state(System::block_number()).unwrap();
},
);
})
}
mod inflation {
use super::*;
#[test]
fn max_staked_rewards_default_not_set_works() {
ExtBuilder::default().build_and_execute(|| {
let default_stakers_payout = validator_payout_for(time_per_era());
assert!(default_stakers_payout > 0);
assert_eq!(<MaxStakedRewards<Test>>::get(), None);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 7500 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
// the final stakers reward is the same as the reward before applied the cap.
assert_eq!(ErasValidatorReward::<Test>::get(0).unwrap(), default_stakers_payout);
})
}
#[test]
fn max_staked_rewards_default_equal_100() {
ExtBuilder::default().build_and_execute(|| {
let default_stakers_payout = validator_payout_for(time_per_era());
assert!(default_stakers_payout > 0);
<MaxStakedRewards<Test>>::set(Some(Percent::from_parts(100)));
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 7500, remainder: 7500 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
// the final stakers reward is the same as the reward before applied the cap.
assert_eq!(ErasValidatorReward::<Test>::get(0).unwrap(), default_stakers_payout);
});
}
#[test]
fn max_staked_rewards_works() {
ExtBuilder::default().nominate(true).build_and_execute(|| {
// sets new max staked rewards through set_staking_configs.
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Noop,
ConfigOp::Set(Percent::from_percent(10)),
));
assert_eq!(<MaxStakedRewards<Test>>::get(), Some(Percent::from_percent(10)));
// check validators account state.
assert_eq!(Session::validators().len(), 2);
assert!(Session::validators().contains(&11) & Session::validators().contains(&21));
// balance of the mock treasury account is 0
assert_eq!(RewardRemainderUnbalanced::get(), 0);
Session::roll_until_active_era(2);
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SessionRotated { starting_session: 4, active_era: 1, planned_era: 2 },
Event::PagedElectionProceeded { page: 0, result: Ok(2) },
Event::SessionRotated { starting_session: 5, active_era: 1, planned_era: 2 },
Event::EraPaid { era_index: 1, validator_payout: 1500, remainder: 13500 },
Event::SessionRotated { starting_session: 6, active_era: 2, planned_era: 2 }
]
);
let treasury_payout = RewardRemainderUnbalanced::get();
let validators_payout = ErasValidatorReward::<Test>::get(1).unwrap();
let total_payout = treasury_payout + validators_payout;
// total payout is the same
assert_eq!(total_payout, total_payout_for(time_per_era()));
// validators get only 10%
assert_eq!(validators_payout, Percent::from_percent(10) * total_payout);
// treasury gets 90%
assert_eq!(treasury_payout, Percent::from_percent(90) * total_payout);
})
}
}
@@ -0,0 +1,52 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
#[test]
fn force_unstake_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(Staking::bonded(&11), Some(11));
// Is bonded -- cannot transfer
assert_noop!(
Balances::transfer_allow_death(RuntimeOrigin::signed(11), 1, 10),
TokenError::FundsUnavailable,
);
// Force unstake requires root.
assert_noop!(Staking::force_unstake(RuntimeOrigin::signed(11), 11, 0), BadOrigin);
assert_ok!(Staking::force_unstake(RuntimeOrigin::root(), 11, 0));
// No longer bonded, can transfer out
assert_eq!(Staking::bonded(&11), None);
assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(11), 1, 10));
});
}
#[test]
fn kill_stash_works() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(Staking::bonded(&11), Some(11));
assert_noop!(Staking::kill_stash(&12), Error::<Test>::NotStash);
assert_ok!(Staking::kill_stash(&11));
assert_eq!(Staking::bonded(&11), None);
});
}
@@ -0,0 +1,856 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
#[test]
fn paired_account_works() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(Staking::bond(RuntimeOrigin::signed(10), 100, RewardDestination::Account(10)));
assert_eq!(<Bonded<Test>>::get(&10), Some(10));
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Controller(10)), Some(10));
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(10)), Some(10));
assert_eq!(<Bonded<Test>>::get(&42), None);
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Controller(42)), None);
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(42)), None);
// bond manually stash with different controller. This is deprecated but the migration
// has not been complete yet (controller: 100, stash: 200)
assert_ok!(bond_controller_stash(100, 200));
assert_eq!(<Bonded<Test>>::get(&200), Some(100));
assert_eq!(
StakingLedger::<Test>::paired_account(StakingAccount::Controller(100)),
Some(200)
);
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(200)), Some(100));
})
}
#[test]
fn get_ledger_works() {
ExtBuilder::default().build_and_execute(|| {
// stash does not exist
assert!(StakingLedger::<Test>::get(StakingAccount::Stash(42)).is_err());
// bonded and paired
assert_eq!(<Bonded<Test>>::get(&11), Some(11));
match StakingLedger::<Test>::get(StakingAccount::Stash(11)) {
Ok(ledger) => {
assert_eq!(ledger.controller(), Some(11));
assert_eq!(ledger.stash, 11);
},
Err(_) => panic!("staking ledger must exist"),
};
// bond manually stash with different controller. This is deprecated but the migration
// has not been complete yet (controller: 100, stash: 200)
assert_ok!(bond_controller_stash(100, 200));
assert_eq!(<Bonded<Test>>::get(&200), Some(100));
match StakingLedger::<Test>::get(StakingAccount::Stash(200)) {
Ok(ledger) => {
assert_eq!(ledger.controller(), Some(100));
assert_eq!(ledger.stash, 200);
},
Err(_) => panic!("staking ledger must exist"),
};
match StakingLedger::<Test>::get(StakingAccount::Controller(100)) {
Ok(ledger) => {
assert_eq!(ledger.controller(), Some(100));
assert_eq!(ledger.stash, 200);
},
Err(_) => panic!("staking ledger must exist"),
};
})
}
#[test]
fn get_ledger_bad_state_fails() {
ExtBuilder::default().has_stakers(false).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// Case 1: double bonded but not corrupted:
// stash 444 has controller 555:
assert_eq!(Bonded::<Test>::get(444), Some(555));
assert_eq!(Ledger::<Test>::get(555).unwrap().stash, 444);
// stash 444 is also a controller of 333:
assert_eq!(Bonded::<Test>::get(333), Some(444));
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(333)), Some(444));
assert_eq!(Ledger::<Test>::get(444).unwrap().stash, 333);
// although 444 is double bonded (it is a controller and a stash of different ledgers),
// we can safely retrieve the ledger and mutate it since the correct ledger is
// returned.
let ledger_result = StakingLedger::<Test>::get(StakingAccount::Stash(444));
assert_eq!(ledger_result.unwrap().stash, 444); // correct ledger.
let ledger_result = StakingLedger::<Test>::get(StakingAccount::Controller(444));
assert_eq!(ledger_result.unwrap().stash, 333); // correct ledger.
// fetching ledger 333 by its stash works.
let ledger_result = StakingLedger::<Test>::get(StakingAccount::Stash(333));
assert_eq!(ledger_result.unwrap().stash, 333);
// Case 2: corrupted ledger bonding.
// in this case, we simulate what happens when fetching a ledger by stash returns a
// ledger with a different stash. when this happens, we return an error instead of the
// ledger to prevent ledger mutations.
let mut ledger = Ledger::<Test>::get(444).unwrap();
assert_eq!(ledger.stash, 333);
ledger.stash = 444;
Ledger::<Test>::insert(444, ledger);
// now, we are prevented from fetching the ledger by stash from 1. It's associated
// controller (2) is now bonding a ledger with a different stash (2, not 1).
assert!(StakingLedger::<Test>::get(StakingAccount::Stash(333)).is_err());
})
}
#[test]
fn bond_works() {
ExtBuilder::default().build_and_execute(|| {
asset::set_stakeable_balance::<T>(&42, 1000);
assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42)));
assert!(<Bonded<Test>>::get(&42).is_none());
let mut ledger: StakingLedger<Test> = StakingLedger::new(42, 84);
let reward_dest = RewardDestination::Account(10);
assert_ok!(ledger.clone().bond(reward_dest));
assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42)));
assert!(<Bonded<Test>>::get(&42).is_some());
assert_eq!(<Payee<Test>>::get(&42), Some(reward_dest));
// cannot bond again.
assert!(ledger.clone().bond(reward_dest).is_err());
// once bonded, unbonding (or any other update) works as expected.
ledger.unlocking = bounded_vec![UnlockChunk { era: 42, value: 42 }];
ledger.active -= 42;
assert_ok!(ledger.update());
})
}
#[test]
fn bond_controller_cannot_be_stash_works() {
ExtBuilder::default().build_and_execute(|| {
let (stash, controller) = testing_utils::create_unique_stash_controller::<Test>(
0,
10,
RewardDestination::Staked,
false,
)
.unwrap();
assert_eq!(Bonded::<Test>::get(stash), Some(controller));
assert_eq!(Ledger::<Test>::get(controller).map(|l| l.stash), Some(stash));
// existing controller should not be able become a stash.
assert_noop!(
Staking::bond(RuntimeOrigin::signed(controller), 10, RewardDestination::Staked),
Error::<Test>::AlreadyPaired,
);
})
}
#[test]
fn is_bonded_works() {
ExtBuilder::default().build_and_execute(|| {
assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42)));
assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Controller(42)));
// adds entry to Bonded without Ledger pair (should not happen).
<Bonded<Test>>::insert(42, 42);
assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Controller(42)));
assert_eq!(<Bonded<Test>>::get(&11), Some(11));
assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Stash(11)));
assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Controller(11)));
<Bonded<Test>>::remove(42); // ensures try-state checks pass.
})
}
#[test]
#[allow(deprecated)]
fn set_payee_errors_on_controller_destination() {
ExtBuilder::default().build_and_execute(|| {
Payee::<Test>::insert(11, RewardDestination::Staked);
assert_noop!(
Staking::set_payee(RuntimeOrigin::signed(11), RewardDestination::Controller),
Error::<Test>::ControllerDeprecated
);
assert_eq!(Payee::<Test>::get(&11), Some(RewardDestination::Staked));
})
}
#[test]
#[allow(deprecated)]
fn update_payee_migration_works() {
ExtBuilder::default().build_and_execute(|| {
// migrate a `Controller` variant to `Account` variant.
Payee::<Test>::insert(11, RewardDestination::Controller);
assert_eq!(Payee::<Test>::get(&11), Some(RewardDestination::Controller));
assert_ok!(Staking::update_payee(RuntimeOrigin::signed(11), 11));
assert_eq!(Payee::<Test>::get(&11), Some(RewardDestination::Account(11)));
// Do not migrate a variant if not `Controller`.
Payee::<Test>::insert(21, RewardDestination::Stash);
assert_eq!(Payee::<Test>::get(&21), Some(RewardDestination::Stash));
assert_noop!(
Staking::update_payee(RuntimeOrigin::signed(11), 21),
Error::<Test>::NotController
);
assert_eq!(Payee::<Test>::get(&21), Some(RewardDestination::Stash));
})
}
#[test]
fn set_controller_with_bad_state_ok() {
ExtBuilder::default().has_stakers(false).nominate(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// in this case, setting controller works due to the ordering of the calls.
assert_ok!(Staking::set_controller(RuntimeOrigin::signed(333)));
assert_ok!(Staking::set_controller(RuntimeOrigin::signed(444)));
assert_ok!(Staking::set_controller(RuntimeOrigin::signed(555)));
})
}
#[test]
fn set_controller_with_bad_state_fails() {
ExtBuilder::default().has_stakers(false).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// setting the controller of ledger associated with stash 555 fails since its stash is a
// controller of another ledger.
assert_noop!(Staking::set_controller(RuntimeOrigin::signed(555)), Error::<Test>::BadState);
assert_noop!(Staking::set_controller(RuntimeOrigin::signed(444)), Error::<Test>::BadState);
assert_ok!(Staking::set_controller(RuntimeOrigin::signed(333)));
})
}
mod deprecate_controller_call {
use super::*;
#[test]
fn deprecate_controller_batch_works_full_weight() {
ExtBuilder::default().try_state(false).build_and_execute(|| {
// Given:
let start = 1001;
let mut controllers: Vec<_> = vec![];
for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
let ctlr: u64 = n.into();
let stash: u64 = (n + 10000).into();
Ledger::<Test>::insert(
ctlr,
StakingLedger {
controller: None,
total: (10 + ctlr).into(),
active: (10 + ctlr).into(),
..StakingLedger::default_from(stash)
},
);
Bonded::<Test>::insert(stash, ctlr);
Payee::<Test>::insert(stash, RewardDestination::Staked);
controllers.push(ctlr);
}
// When:
let bounded_controllers: BoundedVec<
_,
<Test as Config>::MaxControllersInDeprecationBatch,
> = BoundedVec::try_from(controllers).unwrap();
// Only `AdminOrigin` can sign.
assert_noop!(
Staking::deprecate_controller_batch(
RuntimeOrigin::signed(2),
bounded_controllers.clone()
),
BadOrigin
);
let result =
Staking::deprecate_controller_batch(RuntimeOrigin::root(), bounded_controllers);
assert_ok!(result);
assert_eq!(
result.unwrap().actual_weight.unwrap(),
<Test as Config>::WeightInfo::deprecate_controller_batch(
<Test as Config>::MaxControllersInDeprecationBatch::get()
)
);
// Then:
for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
let ctlr: u64 = n.into();
let stash: u64 = (n + 10000).into();
// Ledger no longer keyed by controller.
assert_eq!(Ledger::<Test>::get(ctlr), None);
// Bonded now maps to the stash.
assert_eq!(Bonded::<Test>::get(stash), Some(stash));
// Ledger is now keyed by stash.
let ledger_updated = Ledger::<Test>::get(stash).unwrap();
assert_eq!(ledger_updated.stash, stash);
// Check `active` and `total` values match the original ledger set by controller.
assert_eq!(ledger_updated.active, (10 + ctlr).into());
assert_eq!(ledger_updated.total, (10 + ctlr).into());
}
})
}
#[test]
fn deprecate_controller_batch_works_half_weight() {
ExtBuilder::default().build_and_execute(|| {
// Given:
let start = 1001;
let mut controllers: Vec<_> = vec![];
for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
let ctlr: u64 = n.into();
// Only half of entries are unique pairs.
let stash: u64 = if n % 2 == 0 { (n + 10000).into() } else { ctlr };
Ledger::<Test>::insert(
ctlr,
StakingLedger { controller: None, ..StakingLedger::default_from(stash) },
);
Bonded::<Test>::insert(stash, ctlr);
Payee::<Test>::insert(stash, RewardDestination::Staked);
controllers.push(ctlr);
}
// When:
let bounded_controllers: BoundedVec<
_,
<Test as Config>::MaxControllersInDeprecationBatch,
> = BoundedVec::try_from(controllers.clone()).unwrap();
let result =
Staking::deprecate_controller_batch(RuntimeOrigin::root(), bounded_controllers);
assert_ok!(result);
assert_eq!(
result.unwrap().actual_weight.unwrap(),
<Test as Config>::WeightInfo::deprecate_controller_batch(controllers.len() as u32)
);
// Then:
for n in start..(start + MaxControllersInDeprecationBatch::get()).into() {
let unique_pair = n % 2 == 0;
let ctlr: u64 = n.into();
let stash: u64 = if unique_pair { (n + 10000).into() } else { ctlr };
// Side effect of migration for unique pair.
if unique_pair {
assert_eq!(Ledger::<Test>::get(ctlr), None);
}
// Bonded maps to the stash.
assert_eq!(Bonded::<Test>::get(stash), Some(stash));
// Ledger is keyed by stash.
let ledger_updated = Ledger::<Test>::get(stash).unwrap();
assert_eq!(ledger_updated.stash, stash);
}
})
}
#[test]
fn deprecate_controller_batch_skips_unmigrated_controller_payees() {
ExtBuilder::default().try_state(false).build_and_execute(|| {
// Given:
let stash: u64 = 1000;
let ctlr: u64 = 1001;
Ledger::<Test>::insert(
ctlr,
StakingLedger { controller: None, ..StakingLedger::default_from(stash) },
);
Bonded::<Test>::insert(stash, ctlr);
#[allow(deprecated)]
Payee::<Test>::insert(stash, RewardDestination::Controller);
// When:
let bounded_controllers: BoundedVec<
_,
<Test as Config>::MaxControllersInDeprecationBatch,
> = BoundedVec::try_from(vec![ctlr]).unwrap();
let result =
Staking::deprecate_controller_batch(RuntimeOrigin::root(), bounded_controllers);
assert_ok!(result);
assert_eq!(
result.unwrap().actual_weight.unwrap(),
<Test as Config>::WeightInfo::deprecate_controller_batch(1 as u32)
);
// Then:
// Esure deprecation did not happen.
assert_eq!(Ledger::<Test>::get(ctlr).is_some(), true);
// Bonded still keyed by controller.
assert_eq!(Bonded::<Test>::get(stash), Some(ctlr));
// Ledger is still keyed by controller.
let ledger_updated = Ledger::<Test>::get(ctlr).unwrap();
assert_eq!(ledger_updated.stash, stash);
})
}
#[test]
fn deprecate_controller_batch_with_bad_state_ok() {
ExtBuilder::default().has_stakers(false).nominate(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// now let's deprecate all the controllers for all the existing ledgers.
let bounded_controllers: BoundedVec<
_,
<Test as Config>::MaxControllersInDeprecationBatch,
> = BoundedVec::try_from(vec![333, 444, 555, 777]).unwrap();
assert_ok!(Staking::deprecate_controller_batch(
RuntimeOrigin::root(),
bounded_controllers
));
assert_eq!(
*staking_events().last().unwrap(),
Event::ControllerBatchDeprecated { failures: 0 }
);
})
}
#[test]
fn deprecate_controller_batch_with_bad_state_failures() {
ExtBuilder::default().has_stakers(false).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// now let's deprecate all the controllers for all the existing ledgers.
let bounded_controllers: BoundedVec<
_,
<Test as Config>::MaxControllersInDeprecationBatch,
> = BoundedVec::try_from(vec![777, 555, 444, 333]).unwrap();
assert_ok!(Staking::deprecate_controller_batch(
RuntimeOrigin::root(),
bounded_controllers
));
assert_eq!(
*staking_events().last().unwrap(),
Event::ControllerBatchDeprecated { failures: 2 }
);
})
}
}
mod ledger_recovery {
use super::*;
#[test]
fn inspect_recovery_ledger_simple_works() {
ExtBuilder::default().has_stakers(true).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// non corrupted ledger.
assert_eq!(Staking::inspect_bond_state(&11).unwrap(), LedgerIntegrityState::Ok);
// non bonded stash.
assert!(Bonded::<Test>::get(&1111).is_none());
assert!(Staking::inspect_bond_state(&1111).is_err());
// double bonded but not corrupted.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
})
}
#[test]
fn inspect_recovery_ledger_corupted_killed_works() {
ExtBuilder::default().has_stakers(true).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
let lock_333_before = asset::staked::<Test>(&333);
// get into corrupted and killed ledger state by killing a corrupted ledger:
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
// kill(333)
// (444, 444) -> corrupted and None.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
// now try-state fails.
assert!(Staking::do_try_state(System::block_number()).is_err());
// 333 is corrupted since it's controller is linking 444 ledger.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Corrupted);
// 444 however is OK.
assert_eq!(Staking::inspect_bond_state(&444).unwrap(), LedgerIntegrityState::Ok);
// kill the corrupted ledger that is associated with stash 333.
assert_ok!(StakingLedger::<Test>::kill(&333));
// 333 bond is no more but it returns `BadState` because the lock on this stash is
// still set (see checks below).
assert_eq!(Staking::inspect_bond_state(&333), Err(Error::<Test>::BadState));
// now the *other* ledger associated with 444 has been corrupted and killed (None).
assert_eq!(
Staking::inspect_bond_state(&444),
Ok(LedgerIntegrityState::CorruptedKilled)
);
// side effects on 333 - ledger, bonded, payee, lock should be completely empty.
// however, 333 lock remains.
assert_eq!(asset::staked::<Test>(&333), lock_333_before); // NOK
assert!(Bonded::<Test>::get(&333).is_none()); // OK
assert!(Payee::<Test>::get(&333).is_none()); // OK
assert!(Ledger::<Test>::get(&444).is_none()); // OK
// side effects on 444 - ledger, bonded, payee, lock should remain be intact.
// however, 444 lock was removed.
assert_eq!(asset::staked::<Test>(&444), 0); // NOK
assert!(Bonded::<Test>::get(&444).is_some()); // OK
assert!(Payee::<Test>::get(&444).is_some()); // OK
assert!(Ledger::<Test>::get(&555).is_none()); // NOK
assert!(Staking::do_try_state(System::block_number()).is_err());
})
}
#[test]
fn inspect_recovery_ledger_corupted_killed_other_works() {
ExtBuilder::default().has_stakers(true).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
let lock_333_before = asset::staked::<Test>(&333);
// get into corrupted and killed ledger state by killing a corrupted ledger:
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
// kill(444)
// (333, 444) -> corrupted and None
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
// now try-state fails.
assert!(Staking::do_try_state(System::block_number()).is_err());
// 333 is corrupted since it's controller is linking 444 ledger.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Corrupted);
// 444 however is OK.
assert_eq!(Staking::inspect_bond_state(&444).unwrap(), LedgerIntegrityState::Ok);
// kill the *other* ledger that is double bonded but not corrupted.
assert_ok!(StakingLedger::<Test>::kill(&444));
// now 333 is corrupted and None through the *other* ledger being killed.
assert_eq!(
Staking::inspect_bond_state(&333).unwrap(),
LedgerIntegrityState::CorruptedKilled,
);
// 444 is cleaned and not a stash anymore; no lock left behind.
assert_eq!(Ledger::<Test>::get(&444), None);
assert_eq!(Staking::inspect_bond_state(&444), Err(Error::<Test>::NotStash));
// side effects on 333 - ledger, bonded, payee, lock should be intact.
assert_eq!(asset::staked::<Test>(&333), lock_333_before); // OK
assert_eq!(Bonded::<Test>::get(&333), Some(444)); // OK
assert!(Payee::<Test>::get(&333).is_some());
// however, ledger associated with its controller was killed.
assert!(Ledger::<Test>::get(&444).is_none()); // NOK
// side effects on 444 - ledger, bonded, payee, lock should be completely removed.
assert_eq!(asset::staked::<Test>(&444), 0); // OK
assert!(Bonded::<Test>::get(&444).is_none()); // OK
assert!(Payee::<Test>::get(&444).is_none()); // OK
assert!(Ledger::<Test>::get(&555).is_none()); // OK
assert!(Staking::do_try_state(System::block_number()).is_err());
})
}
#[test]
fn inspect_recovery_ledger_lock_corrupted_works() {
ExtBuilder::default().has_stakers(true).try_state(false).build_and_execute(|| {
setup_double_bonded_ledgers();
// get into lock corrupted ledger state by bond_extra on a ledger that is double bonded
// with a corrupted ledger.
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
// bond_extra(333, 10) -> lock corrupted on 444
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
bond_extra_no_checks(&333, 10);
// now try-state fails.
assert!(Staking::do_try_state(System::block_number()).is_err());
// 333 is corrupted since it's controller is linking 444 ledger.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Corrupted);
// 444 ledger is not corrupted but locks got out of sync.
assert_eq!(
Staking::inspect_bond_state(&444).unwrap(),
LedgerIntegrityState::LockCorrupted
);
})
}
// Corrupted ledger restore.
//
// * Double bonded and corrupted ledger.
#[test]
fn restore_ledger_corrupted_works() {
ExtBuilder::default().has_stakers(true).build_and_execute(|| {
setup_double_bonded_ledgers();
// get into corrupted and killed ledger state.
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Corrupted);
// now try-state fails.
assert!(Staking::do_try_state(System::block_number()).is_err());
// recover the ledger bonded by 333 stash.
assert_ok!(Staking::restore_ledger(RuntimeOrigin::root(), 333, None, None, None));
// try-state checks are ok now.
assert_ok!(Staking::do_try_state(System::block_number()));
})
}
// Corrupted and killed ledger restore.
//
// * Double bonded and corrupted ledger.
// * Ledger killed by own controller.
#[test]
fn restore_ledger_corrupted_killed_works() {
ExtBuilder::default().has_stakers(true).build_and_execute(|| {
setup_double_bonded_ledgers();
// ledger.total == lock
let total_444_before_corruption = asset::staked::<Test>(&444);
// get into corrupted and killed ledger state by killing a corrupted ledger:
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
// kill(333)
// (444, 444) -> corrupted and None.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
// kill the corrupted ledger that is associated with stash 333.
assert_ok!(StakingLedger::<Test>::kill(&333));
// 333 bond is no more but it returns `BadState` because the lock on this stash is
// still set (see checks below).
assert_eq!(Staking::inspect_bond_state(&333), Err(Error::<Test>::BadState));
// now the *other* ledger associated with 444 has been corrupted and killed (None).
assert!(Staking::ledger(StakingAccount::Stash(444)).is_err());
// try-state should fail.
assert!(Staking::do_try_state(System::block_number()).is_err());
// recover the ledger bonded by 333 stash.
assert_ok!(Staking::restore_ledger(RuntimeOrigin::root(), 333, None, None, None));
// for the try-state checks to pass, we also need to recover the stash 444 which is
// corrupted too by proxy of kill(333). Currently, both the lock and the ledger of 444
// have been cleared so we need to provide the new amount to restore the ledger.
assert_noop!(
Staking::restore_ledger(RuntimeOrigin::root(), 444, None, None, None),
Error::<Test>::CannotRestoreLedger
);
assert_ok!(Staking::restore_ledger(
RuntimeOrigin::root(),
444,
None,
Some(total_444_before_corruption),
None,
));
// try-state checks are ok now.
assert_ok!(Staking::do_try_state(System::block_number()));
})
}
// Corrupted and killed by *other* ledger restore.
//
// * Double bonded and corrupted ledger.
// * Ledger killed by own controller.
#[test]
fn restore_ledger_corrupted_killed_other_works() {
ExtBuilder::default().has_stakers(true).build_and_execute(|| {
setup_double_bonded_ledgers();
// get into corrupted and killed ledger state by killing a corrupted ledger:
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
// kill(444)
// (333, 444) -> corrupted and None
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
// now try-state fails.
assert!(Staking::do_try_state(System::block_number()).is_err());
// 333 is corrupted since it's controller is linking 444 ledger.
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Corrupted);
// 444 however is OK.
assert_eq!(Staking::inspect_bond_state(&444).unwrap(), LedgerIntegrityState::Ok);
// kill the *other* ledger that is double bonded but not corrupted.
assert_ok!(StakingLedger::<Test>::kill(&444));
// recover the ledger bonded by 333 stash.
assert_ok!(Staking::restore_ledger(RuntimeOrigin::root(), 333, None, None, None));
// 444 does not need recover in this case since it's been killed successfully.
assert_eq!(Staking::inspect_bond_state(&444), Err(Error::<Test>::NotStash));
// try-state checks are ok now.
assert_ok!(Staking::do_try_state(System::block_number()));
})
}
// Corrupted with bond_extra.
//
// * Double bonded and corrupted ledger.
// * Corrupted ledger calls `bond_extra`
#[test]
fn restore_ledger_corrupted_bond_extra_works() {
ExtBuilder::default().has_stakers(true).build_and_execute(|| {
setup_double_bonded_ledgers();
let lock_333_before = asset::staked::<Test>(&333);
let lock_444_before = asset::staked::<Test>(&444);
// get into corrupted and killed ledger state by killing a corrupted ledger:
// init state:
// (333, 444)
// (444, 555)
// set_controller(444) to 444
// (333, 444) -> corrupted
// (444, 444)
// bond_extra(444, 40) -> OK
// bond_extra(333, 30) -> locks out of sync
assert_eq!(Staking::inspect_bond_state(&333).unwrap(), LedgerIntegrityState::Ok);
set_controller_no_checks(&444);
// now try-state fails.
assert!(Staking::do_try_state(System::block_number()).is_err());
// if 444 bonds extra, the locks remain in sync.
bond_extra_no_checks(&444, 40);
assert_eq!(asset::staked::<Test>(&333), lock_333_before);
assert_eq!(asset::staked::<Test>(&444), lock_444_before + 40);
// however if 333 bonds extra, the wrong lock is updated.
bond_extra_no_checks(&333, 30);
assert_eq!(asset::staked::<Test>(&333), lock_444_before + 40 + 30); //not OK
assert_eq!(asset::staked::<Test>(&444), lock_444_before + 40); // OK
// recover the ledger bonded by 333 stash. Note that the total/lock needs to be
// re-written since on-chain data lock has become out of sync.
assert_ok!(Staking::restore_ledger(
RuntimeOrigin::root(),
333,
None,
Some(lock_333_before + 30),
None
));
// now recover 444 that although it's not corrupted, its lock and ledger.total are out
// of sync. in which case, we need to explicitly set the ledger's lock and amount,
// otherwise the ledger recover will fail.
assert_noop!(
Staking::restore_ledger(RuntimeOrigin::root(), 444, None, None, None),
Error::<Test>::CannotRestoreLedger
);
//and enforcing a new ledger lock/total on this non-corrupted ledger will work.
assert_ok!(Staking::restore_ledger(
RuntimeOrigin::root(),
444,
None,
Some(lock_444_before + 40),
None
));
// double-check that ledgers got to expected state and bond_extra done during the
// corrupted state is part of the recovered ledgers.
let ledger_333 = Bonded::<Test>::get(&333).and_then(Ledger::<Test>::get).unwrap();
let ledger_444 = Bonded::<Test>::get(&444).and_then(Ledger::<Test>::get).unwrap();
assert_eq!(ledger_333.total, lock_333_before + 30);
assert_eq!(asset::staked::<Test>(&333), ledger_333.total);
assert_eq!(ledger_444.total, lock_444_before + 40);
assert_eq!(asset::staked::<Test>(&444), ledger_444.total);
// try-state checks are ok now.
assert_ok!(Staking::do_try_state(System::block_number()));
})
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,99 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Tests for try-state checks.
use super::*;
use pezframe_support::assert_ok;
#[test]
fn try_state_works_with_uninitialized_pallet() {
pezsp_io::TestExternalities::default().execute_with(|| {
// Verify the pallet is uninitialized
assert!(ActiveEra::<Test>::get().is_none());
assert!(CurrentEra::<Test>::get().is_none());
assert_eq!(Bonded::<Test>::iter().count(), 0);
assert_eq!(Ledger::<Test>::iter().count(), 0);
assert_eq!(Validators::<Test>::iter().count(), 0);
assert_eq!(Nominators::<Test>::iter().count(), 0);
// Try-state should pass with uninitialized state
assert_ok!(Staking::do_try_state(System::block_number()));
});
}
#[test]
fn try_state_detects_inconsistent_active_current_era() {
ExtBuilder::default().has_stakers(false).build_and_execute(|| {
// Set only ActiveEra (CurrentEra remains None) - this violates the invariant
ActiveEra::<Test>::put(ActiveEraInfo { index: 1, start: None });
CurrentEra::<Test>::kill();
// Try-state should fail due to inconsistent state
assert!(Staking::do_try_state(System::block_number()).is_err());
// Now set only CurrentEra (ActiveEra None) - this also violates the invariant
ActiveEra::<Test>::kill();
CurrentEra::<Test>::put(1);
// Try-state should fail due to inconsistent state
assert!(Staking::do_try_state(System::block_number()).is_err());
// Both None should pass
ActiveEra::<Test>::kill();
CurrentEra::<Test>::kill();
assert_ok!(Staking::do_try_state(System::block_number()));
// Both Some should pass (assuming other invariants are met)
ActiveEra::<Test>::put(ActiveEraInfo { index: 1, start: None });
CurrentEra::<Test>::put(1);
// Need to set up bonded eras for this to pass
use pezframe_support::BoundedVec;
let bonded_eras: BoundedVec<(u32, u32), _> =
BoundedVec::try_from(vec![(0, 0), (1, 0)]).unwrap();
BondedEras::<Test>::put(bonded_eras);
assert_ok!(Staking::do_try_state(System::block_number()));
});
}
#[test]
fn try_state_bad_exposure() {
ExtBuilder::default().try_state(false).build_and_execute(|| {
Session::roll_until_active_era(2);
assert!(Staking::do_try_state(System::block_number()).is_ok());
let (validator, mut metadata) = ErasStakersOverview::<T>::iter()
.take(1)
.map(|(_era, validator, metadata)| (validator, metadata))
.collect::<Vec<_>>()
.pop()
.unwrap();
metadata.total += 1;
ErasStakersOverview::<T>::insert(2, validator, metadata);
assert!(Staking::do_try_state(System::block_number()).is_err());
});
}
#[test]
fn try_state_bad_eras_total_stake() {
ExtBuilder::default().try_state(false).build_and_execute(|| {
Session::roll_until_active_era(2);
assert!(Staking::do_try_state(System::block_number()).is_ok());
ErasTotalStake::<T>::mutate(2, |s| *s -= 1);
assert!(Staking::do_try_state(System::block_number()).is_err());
});
}
File diff suppressed because it is too large Load Diff