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
+58
View File
@@ -0,0 +1,58 @@
# Changelog
All notable changes and migrations to pezpallet-staking will be documented in this file.
The format is loosely based
on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). We maintain a
single integer version number for staking pallet to keep track of all storage
migrations.
## [v16]
### Added
- New default implementation of `DisablingStrategy` - `UpToLimitWithReEnablingDisablingStrategy`.
Same as `UpToLimitDisablingStrategy` except when a limit (1/3 default) is reached. When limit is
reached the offender is only disabled if his offence is greater or equal than some other already
disabled offender. The smallest possible offender is re-enabled to make space for the new greater
offender. A limit should thus always be respected.
- `DisabledValidators` changed format to include severity of the offence.
## [v15]
### Added
- New trait `DisablingStrategy` which is responsible for making a decision which offenders should be
disabled on new offence.
- Default implementation of `DisablingStrategy` - `UpToLimitDisablingStrategy`. It
disables each new offender up to a threshold (1/3 by default). Offenders are not runtime disabled for
offences in previous era(s). But they will be low-priority node-side disabled for dispute initiation.
- `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all
offenders and if they are disabled or not. The latter just keeps a list of all offenders as they
are disabled by default.
### Deprecated
- `enum DisableStrategy` is no longer needed because disabling is not related to the type of the
offence anymore. A decision if a offender is disabled or not is made by a `DisablingStrategy`
implementation.
## [v14]
### Added
- New item `ErasStakersPaged` that keeps up to `MaxExposurePageSize`
individual nominator exposures by era, validator and page.
- New item `ErasStakersOverview` complementary to `ErasStakersPaged` which keeps
state of own and total stake of the validator across pages.
- New item `ClaimedRewards` to support paged rewards payout.
### Deprecated
- `ErasStakers` and `ErasStakersClipped` is deprecated, will not be used any longer for the exposures of the new era
post v14 and can be removed after 84 eras once all the exposures are stale.
- Field `claimed_rewards` in item `Ledger` is renamed
to `legacy_claimed_rewards` and can be removed after 84 eras.
[v14]: https://github.com/pezkuwichain/kurdistan-sdk/issues/46
+103
View File
@@ -0,0 +1,103 @@
[package]
name = "pezpallet-staking"
version = "28.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "FRAME pallet staking"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { features = ["derive"], workspace = true }
pezframe-election-provider-support = { workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
log = { workspace = true }
pezpallet-authorship = { workspace = true }
pezpallet-session = { features = ["historical"], workspace = true }
scale-info = { features = ["derive", "serde"], workspace = true }
serde = { features = ["alloc", "derive"], workspace = true }
pezsp-application-crypto = { features = ["serde"], workspace = true }
pezsp-io = { workspace = true }
pezsp-runtime = { features = ["serde"], workspace = true }
pezsp-staking = { features = ["serde"], workspace = true }
# Optional imports for benchmarking
pezframe-benchmarking = { optional = true, workspace = true }
rand_chacha = { optional = true, workspace = true }
[dev-dependencies]
pezframe-benchmarking = { workspace = true, default-features = true }
pezframe-election-provider-support = { workspace = true, default-features = true }
pezframe-support = { features = [
"experimental",
], workspace = true, default-features = true }
pezpallet-bags-list = { workspace = true, default-features = true }
pezpallet-balances = { workspace = true, default-features = true }
pezpallet-staking-reward-curve = { workspace = true, default-features = true }
pezpallet-timestamp = { workspace = true, default-features = true }
rand_chacha = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-npos-elections = { workspace = true, default-features = true }
pezsp-tracing = { workspace = true, default-features = true }
bizinikiwi-test-utils = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezframe-benchmarking?/std",
"pezframe-election-provider-support/std",
"pezframe-support/std",
"pezframe-system/std",
"log/std",
"pezpallet-authorship/std",
"pezpallet-bags-list/std",
"pezpallet-balances/std",
"pezpallet-session/std",
"pezpallet-timestamp/std",
"scale-info/std",
"serde/std",
"pezsp-application-crypto/std",
"pezsp-io/std",
"pezsp-npos-elections/std",
"pezsp-runtime/std",
"pezsp-staking/std",
"pezsp-tracing/std",
]
runtime-benchmarks = [
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-election-provider-support/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-authorship/runtime-benchmarks",
"pezpallet-bags-list/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezpallet-session/runtime-benchmarks",
"pezpallet-staking-reward-curve/runtime-benchmarks",
"pezpallet-timestamp/runtime-benchmarks",
"rand_chacha",
"pezsp-io/runtime-benchmarks",
"pezsp-npos-elections/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-staking/runtime-benchmarks",
]
try-runtime = [
"pezframe-election-provider-support/try-runtime",
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-authorship/try-runtime",
"pezpallet-bags-list/try-runtime",
"pezpallet-balances/try-runtime",
"pezpallet-session/try-runtime",
"pezpallet-timestamp/try-runtime",
"pezsp-runtime/try-runtime",
]
+267
View File
@@ -0,0 +1,267 @@
# Staking Module
The Staking module is used to manage funds at stake by network maintainers.
- [`staking::Config`](https://docs.rs/pezpallet-staking/latest/pallet_staking/trait.Config.html)
- [`Call`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html)
- [`Module`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Module.html)
## Overview
The Staking module is the means by which a set of network maintainers (known as _authorities_ in some contexts and
_validators_ in others) are chosen based upon those who voluntarily place funds under deposit. Under deposit, those
funds are rewarded under normal operation but are held at pain of _slash_ (expropriation) should the staked maintainer
be found not to be discharging its duties properly.
### Terminology
<!-- Original author of paragraph: @gavofyork -->
- Staking: The process of locking up funds for some time, placing them at risk of slashing (loss) in order to become a
rewarded maintainer of the network.
- Validating: The process of running a node to actively maintain the network, either by producing blocks or guaranteeing
finality of the chain.
- Nominating: The process of placing staked funds behind one or more validators in order to share in any reward, and
punishment, they take.
- Stash account: The account holding an owner's funds used for staking.
- Controller account (being deprecated): The account that controls an owner's funds for staking.
- Era: A (whole) number of sessions, which is the period that the validator set (and each validator's active nominator
set) is recalculated and where rewards are paid out.
- Slash: The punishment of a staker by reducing its funds.
### Goals
<!-- Original author of paragraph: @gavofyork -->
The staking system in Bizinikiwi NPoS is designed to make the following possible:
- Stake funds that are controlled by a cold wallet.
- Withdraw some, or deposit more, funds without interrupting the role of an entity.
- Switch between roles (nominator, validator, idle) with minimal overhead.
### Scenarios
#### Staking
Almost any interaction with the Staking module requires a process of _**bonding**_ (also known as becoming a _staker_). To
become *bonded*, a fund-holding account known as the _stash account_ (which holds some or all of the funds that become
frozen in place as part of the staking process) gets assigned by the pallet to a _controller account_. The controller account
then issues instructions on how funds shall be used.
An account can become a bonded stash account using the
[`bond`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.bond) call.
Stash accounts can update their associated controller back to their stash account using the
[`set_controller`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.set_controller) call.
Note: Controller accounts are being deprecated in favor of proxy accounts, so it is no longer possible to set a unique
address for a stash's controller.
There are three possible roles that any staked account pair can be in: `Validator`, `Nominator` and `Idle` (defined in
[`StakerStatus`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.StakerStatus.html)). There are three
corresponding instructions to change between roles, namely:
[`validate`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.validate),
[`nominate`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.nominate), and
[`chill`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.chill).
#### Validating
A **validator** takes the role of either validating blocks or ensuring their finality, maintaining the veracity of the
network. A validator should avoid both any sort of malicious misbehavior and going offline. Bonded accounts that state
interest in being a validator do NOT get immediately chosen as a validator. Instead, they are declared as a _candidate_
and they _might_ get elected at the _next era_ as a validator. The result of the election is determined by nominators
and their votes.
An account can become a validator candidate via the
[`validate`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.validate) call.
#### Nomination
A **nominator** does not take any _direct_ role in maintaining the network, instead, it votes on a set of validators to
be elected. Once interest in nomination is stated by an account, it takes effect at the next election round. The funds
in the nominator's stash account indicate the _weight_ of its vote. Both the rewards and any punishment that a validator
earns are shared between the validator and its nominators. This rule incentivizes the nominators to NOT vote for the
misbehaving/offline validators as much as possible, simply because the nominators will also lose funds if they vote
poorly.
An account can become a nominator via the
[`nominate`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.nominate) call.
#### Rewards and Slash
The **reward and slashing** procedure is the core of the Staking module, attempting to _embrace valid behavior_ while
_punishing any misbehavior or lack of availability_.
Rewards must be claimed for each era before it gets too old by `$HISTORY_DEPTH` using the `payout_stakers` call. When a
validator has more than [`Config::MaxExposurePageSize`] nominators, nominators are divided into pages with each call to
`payout_stakers` paying rewards to one page of nominators in a sequential and ascending manner. Any account can also
call `payout_stakers_by_page` to explicitly pay reward for a given page. As evident, this means only the
[`Config::MaxExposurePageSize`] nominators are rewarded per call. This is to limit the i/o cost to mutate storage for
each nominator's account.
Slashing can occur at any point in time, once misbehavior is reported. Once slashing is determined, a value is deducted
from the balance of the validator and all the nominators who voted for this validator (values are deducted from the
_stash_ account of the slashed entity).
Slashing logic is further described in the documentation of the `slashing` module.
Similar to slashing, rewards are also shared among a validator and its associated nominators. Yet, the reward funds are
not always transferred to the stash account and can be configured. See [Reward
Calculation](https://docs.rs/pezpallet-staking/latest/pallet_staking/#reward-calculation) for more details.
#### Chilling
Finally, any of the roles above can choose to step back temporarily and just chill for a while. This means that if they
are a nominator, they will not be considered as voters anymore and if they are validators, they will no longer be a
candidate for the next election.
An account can step back via the
[`chill`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.chill) call.
### Session managing
The module implements the `SessionManager` trait. This is the only API to query new validator sets and to allow these
validator sets to be rewarded once their era is ended.
## Interface
### Dispatchable Functions
The dispatchable functions of the Staking module enable the steps needed for entities to accept and change their role,
alongside some helper functions to get / set the metadata of the module.
### Public Functions
The Staking module contains many public storage items and (im)mutable functions.
## Usage
### Example: Rewarding a validator by id
```rust
use pallet_staking::{self as staking};
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + staking::Config {}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Reward a validator.
#[pallet::weight(0)]
pub fn reward_myself(origin: OriginFor<T>) -> DispatchResult {
let reported = ensure_signed(origin)?;
<staking::Pallet<T>>::reward_by_ids(vec![(reported, 10)]);
Ok(())
}
}
}
```
## Implementation Details
### Era payout
The era payout is computed using a yearly inflation curve defined at
[`T::RewardCurve`](https://docs.rs/pezpallet-staking/latest/pallet_staking/trait.Config.html#associatedtype.RewardCurve) as
such:
```nocompile
staker_payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens / era_per_year
```
This payout is used to reward stakers as defined in next section:
```nocompile
remaining_payout = max_yearly_inflation * total_tokens / era_per_year - staker_payout
```
The remaining reward is sent to the configurable end-point
[`T::RewardRemainder`](https://docs.rs/pezpallet-staking/latest/pallet_staking/trait.Config.html#associatedtype.RewardRemainder).
### Reward Calculation
Validators and nominators are rewarded at the end of each era. The total reward of an era is calculated using the era
duration and the staking rate (the total amount of tokens staked by nominators and validators, divided by the total
token supply). It aims to incentivize toward a defined staking rate. The full specification can be found
[here](https://research.web3.foundation/en/latest/polkadot/economics/1-token-economics.html#inflation-model).
Total reward is split among validators and their nominators depending on the number of points they received during the
era. Points are added to a validator using
[`reward_by_ids`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.reward_by_ids) or
[`reward_by_indices`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.reward_by_indices).
[`Module`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Module.html) implements
[`pallet_authorship::EventHandler`](https://docs.rs/pezpallet-authorship/latest/pallet_authorship/trait.EventHandler.html)
to add reward points to block producers and block producers of referenced uncles.
The validator and its nominator split their reward as follows:
The validator can declare an amount, named
[`commission`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.ValidatorPrefs.html#structfield.commission),
that does not get shared with the nominators at each reward payout through its
[`ValidatorPrefs`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.ValidatorPrefs.html). This value gets
deducted from the total reward that is paid to the validator and its nominators. The remaining portion is split among
the validator and all of its nominators, proportional to the value staked behind this validator (_i.e._ dividing the
[`own`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Exposure.html#structfield.own) or
[`others`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Exposure.html#structfield.others) by
[`total`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Exposure.html#structfield.total) in
[`Exposure`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Exposure.html)).
All entities who receive a reward have the option to choose their reward destination through the
[`Payee`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.Payee.html) storage item (see
[`set_payee`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.set_payee)), to be one of the
following:
- Controller account, (obviously) without increasing the staked value.
- Stash account without increasing the staked value.
- Stash account together with increasing the staked value.
### Additional Fund Management Operations
Any funds already placed into stash can be the target of the following operations:
The controller account can free a portion (or all) of the funds using the
[`unbond`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.unbond) call. Note that the
funds are not immediately accessible. Instead, a duration denoted by
[`BondingDuration`](https://docs.rs/pezpallet-staking/latest/pallet_staking/trait.Config.html#associatedtype.BondingDuration)
(in number of eras) must pass until the funds can actually be removed. Once the `BondingDuration` is over, the
[`withdraw_unbonded`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.withdraw_unbonded)
call can be used to actually withdraw the funds.
Note that there is a limitation to the number of fund-chunks that can be scheduled to be unlocked in the future via
[`unbond`](https://docs.rs/pezpallet-staking/latest/pallet_staking/enum.Call.html#variant.unbond). In case this maximum
(`MAX_UNLOCKING_CHUNKS`) is reached, the bonded account _must_ first wait for a successful call to `withdraw_unbonded`
to remove some of the chunks.
### Election Algorithm
The current election algorithm is implemented based on _Phragmén_. The reference implementation can be found
[here](https://github.com/w3f/consensus/tree/master/NPoS).
The election algorithm, aside from electing the validators with the most stake value and votes, tries to divide the
nominator votes among candidates in an equal manner. To further assure this, an optional post-processing can be applied
that iteratively normalizes the nominator staked values until the total difference among votes of a particular nominator
are less than a threshold.
## GenesisConfig
The Staking module depends on the
[`GenesisConfig`](https://docs.rs/pezpallet-staking/latest/pallet_staking/struct.GenesisConfig.html). The `GenesisConfig`
is optional and allows the setting of some initial stakers.
## Related Modules
- [Balances](https://docs.rs/pezpallet-balances/latest/pallet_balances/): Used to manage values at stake.
- [Session](https://docs.rs/pezpallet-session/latest/pallet_session/): Used to manage sessions. Also, a list of new
validators is stored in the Session module's `Validators` at the end of each era.
License: Apache-2.0
@@ -0,0 +1,30 @@
[package]
name = "pezpallet-staking-reward-curve"
version = "11.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "Reward Curve for FRAME staking pallet"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[lib]
proc-macro = true
[dependencies]
proc-macro-crate = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { features = ["full", "visit"], workspace = true }
[dev-dependencies]
pezsp-runtime = { workspace = true, default-features = true }
[features]
runtime-benchmarks = ["pezsp-runtime/runtime-benchmarks"]
@@ -0,0 +1,452 @@
// 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.
//! Proc macro to generate the reward curve functions and tests.
mod log;
use log::log2;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
/// Accepts a number of expressions to create a instance of PiecewiseLinear which represents the
/// NPoS curve (as detailed
/// [here](https://research.web3.foundation/en/latest/polkadot/overview/2-token-economics.html#inflation-model))
/// for those parameters. Parameters are:
/// - `min_inflation`: the minimal amount to be rewarded between validators, expressed as a fraction
/// of total issuance. Known as `I_0` in the literature. Expressed in millionth, must be between 0
/// and 1_000_000.
///
/// - `max_inflation`: the maximum amount to be rewarded between validators, expressed as a fraction
/// of total issuance. This is attained only when `ideal_stake` is achieved. Expressed in
/// millionth, must be between min_inflation and 1_000_000.
///
/// - `ideal_stake`: the fraction of total issued tokens that should be actively staked behind
/// validators. Known as `x_ideal` in the literature. Expressed in millionth, must be between
/// 0_100_000 and 0_900_000.
///
/// - `falloff`: Known as `decay_rate` in the literature. A co-efficient dictating the strength of
/// the global incentivization to get the `ideal_stake`. A higher number results in less typical
/// inflation at the cost of greater volatility for validators. Expressed in millionth, must be
/// between 0 and 1_000_000.
///
/// - `max_piece_count`: The maximum number of pieces in the curve. A greater number uses more
/// resources but results in higher accuracy. Must be between 2 and 1_000.
///
/// - `test_precision`: The maximum error allowed in the generated test. Expressed in millionth,
/// must be between 0 and 1_000_000.
///
/// # Example
///
/// ```
/// # fn main() {}
/// use pezsp_runtime::curve::PiecewiseLinear;
///
/// pezpallet_staking_reward_curve::build! {
/// const I_NPOS: PiecewiseLinear<'static> = curve!(
/// min_inflation: 0_025_000,
/// max_inflation: 0_100_000,
/// ideal_stake: 0_500_000,
/// falloff: 0_050_000,
/// max_piece_count: 40,
/// test_precision: 0_005_000,
/// );
/// }
/// ```
#[proc_macro]
pub fn build(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as INposInput);
let points = compute_points(&input);
let declaration = generate_piecewise_linear(points);
let test_module = generate_test_module(&input);
let imports = match crate_name("sp-runtime") {
Ok(FoundCrate::Itself) => quote!(
#[doc(hidden)]
pub use pezsp_runtime as _sp_runtime;
),
Ok(FoundCrate::Name(pezsp_runtime)) => {
let ident = syn::Ident::new(&pezsp_runtime, Span::call_site());
quote!( #[doc(hidden)] pub use #ident as _sp_runtime; )
},
Err(e) => match crate_name("pezkuwi-sdk") {
Ok(FoundCrate::Name(pezkuwi_sdk)) => {
let ident = syn::Ident::new(&pezkuwi_sdk, Span::call_site());
quote!( #[doc(hidden)] pub use #ident::pezsp_runtime as _sp_runtime; )
},
_ => syn::Error::new(Span::call_site(), e).to_compile_error(),
},
};
let const_name = input.ident;
let const_type = input.typ;
quote!(
const #const_name: #const_type = {
#imports
#declaration
};
#test_module
)
.into()
}
const MILLION: u32 = 1_000_000;
mod keyword {
syn::custom_keyword!(curve);
syn::custom_keyword!(min_inflation);
syn::custom_keyword!(max_inflation);
syn::custom_keyword!(ideal_stake);
syn::custom_keyword!(falloff);
syn::custom_keyword!(max_piece_count);
syn::custom_keyword!(test_precision);
}
struct INposInput {
ident: syn::Ident,
typ: syn::Type,
min_inflation: u32,
ideal_stake: u32,
max_inflation: u32,
falloff: u32,
max_piece_count: u32,
test_precision: u32,
}
struct Bounds {
min: u32,
min_strict: bool,
max: u32,
max_strict: bool,
}
impl Bounds {
fn check(&self, value: u32) -> bool {
let wrong = (self.min_strict && value <= self.min) ||
(!self.min_strict && value < self.min) ||
(self.max_strict && value >= self.max) ||
(!self.max_strict && value > self.max);
!wrong
}
}
impl core::fmt::Display for Bounds {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}{:07}; {:07}{}",
if self.min_strict { "]" } else { "[" },
self.min,
self.max,
if self.max_strict { "[" } else { "]" },
)
}
}
fn parse_field<Token: Parse + Default + ToTokens>(
input: ParseStream,
bounds: Bounds,
) -> syn::Result<u32> {
<Token>::parse(input)?;
<syn::Token![:]>::parse(input)?;
let value_lit = syn::LitInt::parse(input)?;
let value: u32 = value_lit.base10_parse()?;
if !bounds.check(value) {
return Err(syn::Error::new(
value_lit.span(),
format!(
"Invalid {}: {}, must be in {}",
Token::default().to_token_stream(),
value,
bounds,
),
));
}
Ok(value)
}
impl Parse for INposInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_input;
<syn::Token![const]>::parse(input)?;
let ident = <syn::Ident>::parse(input)?;
<syn::Token![:]>::parse(input)?;
let typ = <syn::Type>::parse(input)?;
<syn::Token![=]>::parse(input)?;
<keyword::curve>::parse(input)?;
<syn::Token![!]>::parse(input)?;
syn::parenthesized!(args_input in input);
<syn::Token![;]>::parse(input)?;
if !input.is_empty() {
return Err(input.error("expected end of input stream, no token expected"));
}
let min_inflation = parse_field::<keyword::min_inflation>(
&args_input,
Bounds { min: 0, min_strict: true, max: 1_000_000, max_strict: false },
)?;
<syn::Token![,]>::parse(&args_input)?;
let max_inflation = parse_field::<keyword::max_inflation>(
&args_input,
Bounds { min: min_inflation, min_strict: true, max: 1_000_000, max_strict: false },
)?;
<syn::Token![,]>::parse(&args_input)?;
let ideal_stake = parse_field::<keyword::ideal_stake>(
&args_input,
Bounds { min: 0_100_000, min_strict: false, max: 0_900_000, max_strict: false },
)?;
<syn::Token![,]>::parse(&args_input)?;
let falloff = parse_field::<keyword::falloff>(
&args_input,
Bounds { min: 0_010_000, min_strict: false, max: 1_000_000, max_strict: false },
)?;
<syn::Token![,]>::parse(&args_input)?;
let max_piece_count = parse_field::<keyword::max_piece_count>(
&args_input,
Bounds { min: 2, min_strict: false, max: 1_000, max_strict: false },
)?;
<syn::Token![,]>::parse(&args_input)?;
let test_precision = parse_field::<keyword::test_precision>(
&args_input,
Bounds { min: 0, min_strict: false, max: 1_000_000, max_strict: false },
)?;
<Option<syn::Token![,]>>::parse(&args_input)?;
if !args_input.is_empty() {
return Err(args_input.error("expected end of input stream, no token expected"));
}
Ok(Self {
ident,
typ,
min_inflation,
ideal_stake,
max_inflation,
falloff,
max_piece_count,
test_precision,
})
}
}
struct INPoS {
i_0: u32,
i_ideal_times_x_ideal: u32,
i_ideal: u32,
x_ideal: u32,
d: u32,
}
impl INPoS {
fn from_input(input: &INposInput) -> Self {
INPoS {
i_0: input.min_inflation,
i_ideal: (input.max_inflation as u64 * MILLION as u64 / input.ideal_stake as u64)
.try_into()
.unwrap(),
i_ideal_times_x_ideal: input.max_inflation,
x_ideal: input.ideal_stake,
d: input.falloff,
}
}
// calculates x from:
// y = i_0 + (i_ideal * x_ideal - i_0) * 2^((x_ideal - x)/d)
// See web3 docs for the details
fn compute_opposite_after_x_ideal(&self, y: u32) -> u32 {
if y == self.i_0 {
return u32::MAX;
}
// Note: the log term calculated here represents a per_million value
let log = log2(self.i_ideal_times_x_ideal - self.i_0, y - self.i_0);
let term: u32 = ((self.d as u64 * log as u64) / 1_000_000).try_into().unwrap();
self.x_ideal + term
}
}
fn compute_points(input: &INposInput) -> Vec<(u32, u32)> {
let inpos = INPoS::from_input(input);
let mut points = vec![(0, inpos.i_0), (inpos.x_ideal, inpos.i_ideal_times_x_ideal)];
// For each point p: (next_p.0 - p.0) < segment_length && (next_p.1 - p.1) < segment_length.
// This ensures that the total number of segments doesn't overflow max_piece_count.
let max_length = (input.max_inflation - input.min_inflation + 1_000_000 - inpos.x_ideal) /
(input.max_piece_count - 1);
let mut delta_y = max_length;
let mut y = input.max_inflation;
// The algorithm divides the curve in segments with vertical and horizontal lenghts less
// than `max_length`. This is not very accurate in case of very consequent step.
while delta_y != 0 {
let next_y = y - delta_y;
if next_y <= input.min_inflation {
delta_y = delta_y.saturating_sub(1);
continue;
}
let next_x = inpos.compute_opposite_after_x_ideal(next_y);
if (next_x - points.last().unwrap().0) > max_length {
delta_y = delta_y.saturating_sub(1);
continue;
}
if next_x >= 1_000_000 {
let prev = points.last().unwrap();
// Compute the y corresponding to x=1_000_000 using the current point and the previous
// one.
let delta_y: u32 = ((next_x - 1_000_000) as u64 * (prev.1 - next_y) as u64 /
(next_x - prev.0) as u64)
.try_into()
.unwrap();
let y = next_y + delta_y;
points.push((1_000_000, y));
return points;
}
points.push((next_x, next_y));
y = next_y;
}
points.push((1_000_000, inpos.i_0));
points
}
fn generate_piecewise_linear(points: Vec<(u32, u32)>) -> TokenStream2 {
let mut points_tokens = quote!();
let max = points
.iter()
.map(|&(_, x)| x)
.max()
.unwrap_or(0)
.checked_mul(1_000)
// clip at 1.0 for sanity only since it'll panic later if too high.
.unwrap_or(1_000_000_000);
for (x, y) in points {
let error = || {
panic!(
"Generated reward curve approximation doesn't fit into [0, 1] -> [0, 1] because \
of point:
x = {:07} per million
y = {:07} per million",
x, y
)
};
let x_perbill = x.checked_mul(1_000).unwrap_or_else(error);
let y_perbill = y.checked_mul(1_000).unwrap_or_else(error);
points_tokens.extend(quote!(
(
_sp_runtime::Perbill::from_parts(#x_perbill),
_sp_runtime::Perbill::from_parts(#y_perbill),
),
));
}
quote!(
_sp_runtime::curve::PiecewiseLinear::<'static> {
points: & [ #points_tokens ],
maximum: _sp_runtime::Perbill::from_parts(#max),
}
)
}
fn generate_test_module(input: &INposInput) -> TokenStream2 {
let inpos = INPoS::from_input(input);
let ident = &input.ident;
let precision = input.test_precision;
let i_0 = inpos.i_0 as f64 / MILLION as f64;
let i_ideal_times_x_ideal = inpos.i_ideal_times_x_ideal as f64 / MILLION as f64;
let i_ideal = inpos.i_ideal as f64 / MILLION as f64;
let x_ideal = inpos.x_ideal as f64 / MILLION as f64;
let d = inpos.d as f64 / MILLION as f64;
let max_piece_count = input.max_piece_count;
quote!(
#[cfg(test)]
mod __pallet_staking_reward_curve_test_module {
fn i_npos(x: f64) -> f64 {
if x <= #x_ideal {
#i_0 + x * (#i_ideal - #i_0 / #x_ideal)
} else {
#i_0 + (#i_ideal_times_x_ideal - #i_0) * 2_f64.powf((#x_ideal - x) / #d)
}
}
const MILLION: u32 = 1_000_000;
#[test]
fn reward_curve_precision() {
for &base in [MILLION, u32::MAX].iter() {
let number_of_check = 100_000.min(base);
for check_index in 0..=number_of_check {
let i = (check_index as u64 * base as u64 / number_of_check as u64) as u32;
let x = i as f64 / base as f64;
let float_res = (i_npos(x) * base as f64).round() as u32;
let int_res = super::#ident.calculate_for_fraction_times_denominator(i, base);
let err = (
(float_res.max(int_res) - float_res.min(int_res)) as u64
* MILLION as u64
/ float_res as u64
) as u32;
if err > #precision {
panic!("\n\
Generated reward curve approximation differ from real one:\n\t\
for i = {} and base = {}, f(i/base) * base = {},\n\t\
but approximation = {},\n\t\
err = {:07} millionth,\n\t\
try increase the number of segment: {} or the test_error: {}.\n",
i, base, float_res, int_res, err, #max_piece_count, #precision
);
}
}
}
}
#[test]
fn reward_curve_piece_count() {
assert!(
super::#ident.points.len() as u32 - 1 <= #max_piece_count,
"Generated reward curve approximation is invalid: \
has more points than specified, please fill an issue."
);
}
}
)
}
@@ -0,0 +1,142 @@
// 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.
/// Simple u32 power of 2 function - simply uses a bit shift
macro_rules! pow2 {
($n:expr) => {
1_u32 << $n
};
}
/// Returns the k_th per_million taylor term for a log2 function
fn taylor_term(k: u32, y_num: u128, y_den: u128) -> u32 {
let _2_div_ln_2: u128 = 2_885_390u128;
if k == 0 {
(_2_div_ln_2 * (y_num).pow(1) / (y_den).pow(1)).try_into().unwrap()
} else {
let mut res = _2_div_ln_2 * (y_num).pow(3) / (y_den).pow(3);
for _ in 1..k {
res = res * (y_num).pow(2) / (y_den).pow(2);
}
res /= 2 * k as u128 + 1;
res.try_into().unwrap()
}
}
/// Performs a log2 operation using a rational fraction
///
/// result = log2(p/q) where p/q is bound to [1, 1_000_000]
/// Where:
/// * q represents the numerator of the rational fraction input
/// * p represents the denominator of the rational fraction input
/// * result represents a per-million output of log2
pub fn log2(p: u32, q: u32) -> u32 {
assert!(p >= q); // keep p/q bound to [1, inf)
assert!(p <= u32::MAX / 2);
// This restriction should not be mandatory. But function is only tested and used for this.
assert!(p <= 1_000_000);
assert!(q <= 1_000_000);
// log2(1) = 0
if p == q {
return 0;
}
// find the power of 2 where q * 2^n <= p < q * 2^(n+1)
let mut n = 0u32;
while (p < pow2!(n) * q) || (p >= pow2!(n + 1) * q) {
n += 1;
assert!(n < 32); // cannot represent 2^32 in u32
}
assert!(p < pow2!(n + 1) * q);
let y_num: u32 = p - pow2!(n) * q;
let y_den: u32 = p + pow2!(n) * q;
// Loop through each Taylor series coefficient until it reaches 10^-6
let mut res = n * 1_000_000u32;
let mut k = 0;
loop {
let term = taylor_term(k, y_num.into(), y_den.into());
if term == 0 {
break;
}
res += term;
k += 1;
}
res
}
#[test]
fn test_log() {
let div = 1_000;
for p in 0..=div {
for q in 1..=p {
let p: u32 = (1_000_000 as u64 * p as u64 / div as u64).try_into().unwrap();
let q: u32 = (1_000_000 as u64 * q as u64 / div as u64).try_into().unwrap();
let res = -(log2(p, q) as i64);
let expected = ((q as f64 / p as f64).log(2.0) * 1_000_000 as f64).round() as i64;
assert!((res - expected).abs() <= 6);
}
}
}
#[test]
#[should_panic]
fn test_log_p_must_be_greater_than_q() {
let p: u32 = 1_000;
let q: u32 = 1_001;
let _ = log2(p, q);
}
#[test]
#[should_panic]
fn test_log_p_upper_bound() {
let p: u32 = 1_000_001;
let q: u32 = 1_000_000;
let _ = log2(p, q);
}
#[test]
#[should_panic]
fn test_log_q_limit() {
let p: u32 = 1_000_000;
let q: u32 = 0;
let _ = log2(p, q);
}
#[test]
fn test_log_of_one_boundary() {
let p: u32 = 1_000_000;
let q: u32 = 1_000_000;
assert_eq!(log2(p, q), 0);
}
#[test]
fn test_log_of_largest_input() {
let p: u32 = 1_000_000;
let q: u32 = 1;
let expected = 19_931_568;
let tolerance = 100;
assert!((log2(p, q) as i32 - expected as i32).abs() < tolerance);
}
@@ -0,0 +1,45 @@
// 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.
//! Test crate for pezpallet-staking-reward-curve. Allows to test for procedural macro.
//! See tests directory.
mod test_small_falloff {
pezpallet_staking_reward_curve::build! {
const REWARD_CURVE: pezsp_runtime::curve::PiecewiseLinear<'static> = curve!(
min_inflation: 0_020_000,
max_inflation: 0_200_000,
ideal_stake: 0_600_000,
falloff: 0_010_000,
max_piece_count: 200,
test_precision: 0_005_000,
);
}
}
mod test_big_falloff {
pezpallet_staking_reward_curve::build! {
const REWARD_CURVE: pezsp_runtime::curve::PiecewiseLinear<'static> = curve!(
min_inflation: 0_100_000,
max_inflation: 0_400_000,
ideal_stake: 0_400_000,
falloff: 1_000_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
}
@@ -0,0 +1,23 @@
[package]
name = "pezpallet-staking-reward-fn"
version = "19.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "Reward function for FRAME staking pallet"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
log = { workspace = true }
pezsp-arithmetic = { workspace = true }
[features]
default = ["std"]
std = ["log/std", "pezsp-arithmetic/std"]
@@ -0,0 +1,224 @@
// 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.
#![cfg_attr(not(feature = "std"), no_std)]
//! Useful function for inflation for nominated proof of stake.
use pezsp_arithmetic::{
biguint::BigUint,
traits::{SaturatedConversion, Zero},
PerThing, Perquintill,
};
/// Compute yearly inflation using function
///
/// ```ignore
/// I(x) = for x between 0 and x_ideal: x / x_ideal,
/// for x between x_ideal and 1: 2^((x_ideal - x) / d)
/// ```
///
/// where:
/// * x is the stake rate, i.e. fraction of total issued tokens that actively staked behind
/// validators.
/// * d is the falloff or `decay_rate`
/// * x_ideal: the ideal stake rate.
///
/// The result is meant to be scaled with minimum inflation and maximum inflation.
///
/// (as detailed
/// [here](https://research.web3.foundation/Polkadot/overview/token-economics#inflation-model-with-parachains))
///
/// Arguments are:
/// * `stake`: The fraction of total issued tokens that actively staked behind validators. Known as
/// `x` in the literature. Must be between 0 and 1.
/// * `ideal_stake`: The fraction of total issued tokens that should be actively staked behind
/// validators. Known as `x_ideal` in the literature. Must be between 0 and 1.
/// * `falloff`: Known as `decay_rate` in the literature. A co-efficient dictating the strength of
/// the global incentivization to get the `ideal_stake`. A higher number results in less typical
/// inflation at the cost of greater volatility for validators. Must be more than 0.01.
pub fn compute_inflation<P: PerThing>(stake: P, ideal_stake: P, falloff: P) -> P {
if stake < ideal_stake {
// ideal_stake is more than 0 because it is strictly more than stake
return stake / ideal_stake;
}
if falloff < P::from_percent(1.into()) {
log::error!("Invalid inflation computation: falloff less than 1% is not supported");
return PerThing::zero();
}
let accuracy = {
let mut a = BigUint::from(Into::<u128>::into(P::ACCURACY));
a.lstrip();
a
};
let mut falloff = BigUint::from(falloff.deconstruct().into());
falloff.lstrip();
let ln2 = {
/// `ln(2)` expressed in as perquintillionth.
const LN2: u64 = 0_693_147_180_559_945_309;
let ln2 = P::from_rational(LN2.into(), Perquintill::ACCURACY.into());
BigUint::from(ln2.deconstruct().into())
};
// falloff is stripped above.
let ln2_div_d = div_by_stripped(ln2.mul(&accuracy), &falloff);
let inpos_param = INPoSParam {
x_ideal: BigUint::from(ideal_stake.deconstruct().into()),
x: BigUint::from(stake.deconstruct().into()),
accuracy,
ln2_div_d,
};
let res = compute_taylor_serie_part(&inpos_param);
match u128::try_from(res.clone()) {
Ok(res) if res <= Into::<u128>::into(P::ACCURACY) => P::from_parts(res.saturated_into()),
// If result is beyond bounds there is nothing we can do
_ => {
log::error!("Invalid inflation computation: unexpected result {:?}", res);
P::zero()
},
}
}
/// Internal struct holding parameter info alongside other cached value.
///
/// All expressed in part from `accuracy`
struct INPoSParam {
ln2_div_d: BigUint,
x_ideal: BigUint,
x: BigUint,
/// Must be stripped and have no leading zeros.
accuracy: BigUint,
}
/// Compute `2^((x_ideal - x) / d)` using taylor series.
///
/// x must be strictly more than x_ideal.
///
/// result is expressed with accuracy `INPoSParam.accuracy`
fn compute_taylor_serie_part(p: &INPoSParam) -> BigUint {
// The last computed taylor term.
let mut last_taylor_term = p.accuracy.clone();
// Whereas taylor sum is positive.
let mut taylor_sum_positive = true;
// The sum of all taylor term.
let mut taylor_sum = last_taylor_term.clone();
for k in 1..300 {
last_taylor_term = compute_taylor_term(k, &last_taylor_term, p);
if last_taylor_term.is_zero() {
break;
}
let last_taylor_term_positive = k % 2 == 0;
if taylor_sum_positive == last_taylor_term_positive {
taylor_sum = taylor_sum.add(&last_taylor_term);
} else if taylor_sum >= last_taylor_term {
taylor_sum = taylor_sum
.sub(&last_taylor_term)
// NOTE: Should never happen as checked above
.unwrap_or_else(|e| e);
} else {
taylor_sum_positive = !taylor_sum_positive;
taylor_sum = last_taylor_term
.clone()
.sub(&taylor_sum)
// NOTE: Should never happen as checked above
.unwrap_or_else(|e| e);
}
}
if !taylor_sum_positive {
return BigUint::zero();
}
taylor_sum.lstrip();
taylor_sum
}
/// Return the absolute value of k-th taylor term of `2^((x_ideal - x))/d` i.e.
/// `((x - x_ideal) * ln(2) / d)^k / k!`
///
/// x must be strictly more x_ideal.
///
/// We compute the term from the last term using this formula:
///
/// `((x - x_ideal) * ln(2) / d)^k / k! == previous_term * (x - x_ideal) * ln(2) / d / k`
///
/// `previous_taylor_term` and result are expressed with accuracy `INPoSParam.accuracy`
fn compute_taylor_term(k: u32, previous_taylor_term: &BigUint, p: &INPoSParam) -> BigUint {
let x_minus_x_ideal =
p.x.clone()
.sub(&p.x_ideal)
// NOTE: Should never happen, as x must be more than x_ideal
.unwrap_or_else(|_| BigUint::zero());
let res = previous_taylor_term.clone().mul(&x_minus_x_ideal).mul(&p.ln2_div_d).div_unit(k);
// p.accuracy is stripped by definition.
let res = div_by_stripped(res, &p.accuracy);
let mut res = div_by_stripped(res, &p.accuracy);
res.lstrip();
res
}
/// Compute a div b.
///
/// requires `b` to be stripped and have no leading zeros.
fn div_by_stripped(mut a: BigUint, b: &BigUint) -> BigUint {
a.lstrip();
if b.len() == 0 {
log::error!("Computation error: Invalid division");
return BigUint::zero();
}
if b.len() == 1 {
return a.div_unit(b.checked_get(0).unwrap_or(1));
}
if b.len() > a.len() {
return BigUint::zero();
}
if b.len() == a.len() {
// 100_000^2 is more than 2^32-1, thus `new_a` has more limbs than `b`.
let mut new_a = a.mul(&BigUint::from(100_000u64.pow(2)));
new_a.lstrip();
debug_assert!(new_a.len() > b.len());
return new_a
.div(b, false)
.map(|res| res.0)
.unwrap_or_else(BigUint::zero)
.div_unit(100_000)
.div_unit(100_000);
}
a.div(b, false).map(|res| res.0).unwrap_or_else(BigUint::zero)
}
@@ -0,0 +1,101 @@
// 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 pezsp_arithmetic::{PerThing, PerU16, Perbill, Percent, Perquintill};
/// This test the precision and panics if error too big error.
///
/// error is asserted to be less or equal to 8/accuracy or 8*f64::EPSILON
fn test_precision<P: PerThing>(stake: P, ideal_stake: P, falloff: P) {
let accuracy_f64 = Into::<u128>::into(P::ACCURACY) as f64;
let res = pezpallet_staking_reward_fn::compute_inflation(stake, ideal_stake, falloff);
let res = Into::<u128>::into(res.deconstruct()) as f64 / accuracy_f64;
let expect = float_i_npos(stake, ideal_stake, falloff);
let error = (res - expect).abs();
if error > 8f64 / accuracy_f64 && error > 8.0 * f64::EPSILON {
panic!(
"stake: {:?}, ideal_stake: {:?}, falloff: {:?}, res: {}, expect: {}",
stake, ideal_stake, falloff, res, expect
);
}
}
/// compute the inflation using floats
fn float_i_npos<P: PerThing>(stake: P, ideal_stake: P, falloff: P) -> f64 {
let accuracy_f64 = Into::<u128>::into(P::ACCURACY) as f64;
let ideal_stake = Into::<u128>::into(ideal_stake.deconstruct()) as f64 / accuracy_f64;
let stake = Into::<u128>::into(stake.deconstruct()) as f64 / accuracy_f64;
let falloff = Into::<u128>::into(falloff.deconstruct()) as f64 / accuracy_f64;
let x_ideal = ideal_stake;
let x = stake;
let d = falloff;
if x < x_ideal {
x / x_ideal
} else {
2_f64.powf((x_ideal - x) / d)
}
}
#[test]
fn test_precision_for_minimum_falloff() {
fn test_falloff_precision_for_minimum_falloff<P: PerThing>() {
for stake in 0..1_000 {
let stake = P::from_rational(stake, 1_000);
let ideal_stake = P::zero();
let falloff = P::from_rational(1, 100);
test_precision(stake, ideal_stake, falloff);
}
}
test_falloff_precision_for_minimum_falloff::<Perquintill>();
test_falloff_precision_for_minimum_falloff::<PerU16>();
test_falloff_precision_for_minimum_falloff::<Perbill>();
test_falloff_precision_for_minimum_falloff::<Percent>();
}
#[test]
fn compute_inflation_works() {
fn compute_inflation_works<P: PerThing>() {
for stake in 0..100 {
for ideal_stake in 0..10 {
for falloff in 1..10 {
let stake = P::from_rational(stake, 100);
let ideal_stake = P::from_rational(ideal_stake, 10);
let falloff = P::from_rational(falloff, 100);
test_precision(stake, ideal_stake, falloff);
}
}
}
}
compute_inflation_works::<Perquintill>();
compute_inflation_works::<PerU16>();
compute_inflation_works::<Perbill>();
compute_inflation_works::<Percent>();
}
@@ -0,0 +1,29 @@
[package]
name = "pezpallet-staking-runtime-api"
version = "14.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "RPC runtime API for transaction payment FRAME pallet"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { features = ["derive"], workspace = true }
pezsp-api = { workspace = true }
pezsp-staking = { workspace = true }
[features]
default = ["std"]
std = ["codec/std", "pezsp-api/std", "pezsp-staking/std"]
runtime-benchmarks = [
"pezsp-api/runtime-benchmarks",
"pezsp-staking/runtime-benchmarks",
]
@@ -0,0 +1,3 @@
Runtime API definition for the staking pallet.
License: Apache-2.0
@@ -0,0 +1,39 @@
// 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.
//! Runtime API definition for the staking pallet.
#![cfg_attr(not(feature = "std"), no_std)]
use codec::Codec;
pezsp_api::decl_runtime_apis! {
pub trait StakingApi<Balance, AccountId>
where
Balance: Codec,
AccountId: Codec,
{
/// Returns the nominations quota for a nominator with a given balance.
fn nominations_quota(balance: Balance) -> u32;
/// Returns the page count of exposures for a validator `account` in a given era.
fn eras_stakers_page_count(era: pezsp_staking::EraIndex, account: AccountId) -> pezsp_staking::Page;
/// Returns true if a validator `account` has pages to be claimed for the given era.
fn pending_rewards(era: pezsp_staking::EraIndex, account: AccountId) -> bool;
}
}
+157
View File
@@ -0,0 +1,157 @@
// 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)
}
/// 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);
}
/// 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)
}
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::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,108 @@
// 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.
//! This module expose one function `P_NPoS` (Payout NPoS) or `compute_total_payout` which returns
//! the total payout for the era given the era duration and the staking rate in NPoS.
//! The staking rate in NPoS is the total amount of tokens staked by nominators and validators,
//! divided by the total token supply.
use pezsp_runtime::{curve::PiecewiseLinear, traits::AtLeast32BitUnsigned, Perbill};
/// The total payout to all validators (and their nominators) per era and maximum payout.
///
/// Defined as such:
/// `staker-payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokens /
/// era_per_year` `maximum-payout = max_yearly_inflation * total_tokens / era_per_year`
///
/// `era_duration` is expressed in millisecond.
pub fn compute_total_payout<N>(
yearly_inflation: &PiecewiseLinear<'static>,
npos_token_staked: N,
total_tokens: N,
era_duration: u64,
) -> (N, N)
where
N: AtLeast32BitUnsigned + Clone,
{
// Milliseconds per year for the Julian year (365.25 days).
const MILLISECONDS_PER_YEAR: u64 = 1000 * 3600 * 24 * 36525 / 100;
let portion = Perbill::from_rational(era_duration as u64, MILLISECONDS_PER_YEAR);
let payout = portion *
yearly_inflation
.calculate_for_fraction_times_denominator(npos_token_staked, total_tokens.clone());
let maximum = portion * (yearly_inflation.maximum * total_tokens);
(payout, maximum)
}
#[cfg(test)]
mod test {
use pezsp_runtime::curve::PiecewiseLinear;
pezpallet_staking_reward_curve::build! {
const I_NPOS: PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
#[test]
fn npos_curve_is_sensible() {
const YEAR: u64 = 365 * 24 * 60 * 60 * 1000;
// check maximum inflation.
// not 10_000 due to rounding error.
assert_eq!(super::compute_total_payout(&I_NPOS, 0, 100_000u64, YEAR).1, 9_993);
// super::I_NPOS.calculate_for_fraction_times_denominator(25, 100)
assert_eq!(super::compute_total_payout(&I_NPOS, 0, 100_000u64, YEAR).0, 2_498);
assert_eq!(super::compute_total_payout(&I_NPOS, 5_000, 100_000u64, YEAR).0, 3_248);
assert_eq!(super::compute_total_payout(&I_NPOS, 25_000, 100_000u64, YEAR).0, 6_246);
assert_eq!(super::compute_total_payout(&I_NPOS, 40_000, 100_000u64, YEAR).0, 8_494);
assert_eq!(super::compute_total_payout(&I_NPOS, 50_000, 100_000u64, YEAR).0, 9_993);
assert_eq!(super::compute_total_payout(&I_NPOS, 60_000, 100_000u64, YEAR).0, 4_379);
assert_eq!(super::compute_total_payout(&I_NPOS, 75_000, 100_000u64, YEAR).0, 2_733);
assert_eq!(super::compute_total_payout(&I_NPOS, 95_000, 100_000u64, YEAR).0, 2_513);
assert_eq!(super::compute_total_payout(&I_NPOS, 100_000, 100_000u64, YEAR).0, 2_505);
const DAY: u64 = 24 * 60 * 60 * 1000;
assert_eq!(super::compute_total_payout(&I_NPOS, 25_000, 100_000u64, DAY).0, 17);
assert_eq!(super::compute_total_payout(&I_NPOS, 50_000, 100_000u64, DAY).0, 27);
assert_eq!(super::compute_total_payout(&I_NPOS, 75_000, 100_000u64, DAY).0, 7);
const SIX_HOURS: u64 = 6 * 60 * 60 * 1000;
assert_eq!(super::compute_total_payout(&I_NPOS, 25_000, 100_000u64, SIX_HOURS).0, 4);
assert_eq!(super::compute_total_payout(&I_NPOS, 50_000, 100_000u64, SIX_HOURS).0, 7);
assert_eq!(super::compute_total_payout(&I_NPOS, 75_000, 100_000u64, SIX_HOURS).0, 2);
const HOUR: u64 = 60 * 60 * 1000;
assert_eq!(
super::compute_total_payout(
&I_NPOS,
2_500_000_000_000_000_000_000_000_000u128,
5_000_000_000_000_000_000_000_000_000u128,
HOUR
)
.0,
57_038_500_000_000_000_000_000
);
}
}
+307
View File
@@ -0,0 +1,307 @@
// 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 pezframe_support::{defensive, ensure, traits::Defensive};
use pezsp_runtime::DispatchResult;
use pezsp_staking::{StakingAccount, StakingInterface};
use crate::{
asset, BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination,
StakingLedger, VirtualStakers,
};
#[cfg(any(feature = "runtime-benchmarks", test))]
use pezsp_runtime::traits::Zero;
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(),
legacy_claimed_rewards: 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(),
legacy_claimed_rewards: 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.clone() {
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
/// locks 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)?;
}
Ok(())
})?
}
}
#[cfg(test)]
use {
crate::UnlockChunk,
codec::{Decode, Encode, MaxEncodedLen},
scale_info::TypeInfo,
};
// 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<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
pub legacy_claimed_rewards: pezframe_support::BoundedVec<pezsp_staking::EraIndex, T::HistoryDepth>,
}
#[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 &&
self.legacy_claimed_rewards == other.legacy_claimed_rewards
}
}
#[cfg(test)]
impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,437 @@
// 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
//! Storage migrations for the Staking pallet. The changelog for this is maintained at
//! [CHANGELOG.md](https://github.com/pezkuwichain/pezkuwi-sdk/blob/master/bizinikiwi/pezframe/staking/CHANGELOG.md).
use super::*;
use pezframe_support::{
migrations::VersionedMigration,
pezpallet_prelude::ValueQuery,
storage_alias,
traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade},
};
#[cfg(feature = "try-runtime")]
use pezsp_runtime::TryRuntimeError;
/// Used for release versioning up to v12.
///
/// Obsolete from v13. Keeping around to make encoding/decoding of old migration code easier.
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
enum ObsoleteReleases {
V5_0_0, // blockable validators.
V6_0_0, // removal of all storage associated with offchain phragmen.
V7_0_0, // keep track of number of nominators / validators in map
V8_0_0, // populate `VoterList`.
V9_0_0, // inject validators into `VoterList` as well.
V10_0_0, // remove `EarliestUnappliedSlash`.
V11_0_0, // Move pallet storage prefix, e.g. BagsList -> VoterBagsList
V12_0_0, // remove `HistoryDepth`.
}
impl Default for ObsoleteReleases {
fn default() -> Self {
ObsoleteReleases::V12_0_0
}
}
/// Alias to the old storage item used for release versioning. Obsolete since v13.
#[storage_alias]
type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;
/// Supports the migration of Validator Disabling from pezpallet-staking to pezpallet-session
pub mod v17 {
use super::*;
#[pezframe_support::storage_alias]
pub type DisabledValidators<T: Config> =
StorageValue<Pallet<T>, BoundedVec<(u32, OffenceSeverity), ConstU32<333>>, ValueQuery>;
pub struct MigrateDisabledToSession<T>(core::marker::PhantomData<T>);
impl<T: Config> pezpallet_session::migrations::v1::MigrateDisabledValidators
for MigrateDisabledToSession<T>
{
#[cfg(feature = "try-runtime")]
fn peek_disabled() -> Vec<(u32, OffenceSeverity)> {
DisabledValidators::<T>::get().into()
}
fn take_disabled() -> Vec<(u32, OffenceSeverity)> {
DisabledValidators::<T>::take().into()
}
}
}
/// Migrating `DisabledValidators` from `Vec<u32>` to `Vec<(u32, OffenceSeverity)>` to track offense
/// severity for re-enabling purposes.
pub mod v16 {
use super::*;
use pezsp_staking::offence::OffenceSeverity;
#[pezframe_support::storage_alias]
pub(crate) type DisabledValidators<T: Config> =
StorageValue<Pallet<T>, Vec<(u32, OffenceSeverity)>, ValueQuery>;
pub struct VersionUncheckedMigrateV15ToV16<T>(core::marker::PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV15ToV16<T> {
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::TryRuntimeError> {
let old_disabled_validators = v15::DisabledValidators::<T>::get();
Ok(old_disabled_validators.encode())
}
fn on_runtime_upgrade() -> Weight {
// Migrating `DisabledValidators` from `Vec<u32>` to `Vec<(u32, OffenceSeverity)>`.
// Using max severity (PerBill 100%) for the migration which effectively makes it so
// offenders before the migration will not be re-enabled this era unless there are
// other 100% offenders.
let max_offence = OffenceSeverity(Perbill::from_percent(100));
// Inject severity
let migrated = v15::DisabledValidators::<T>::take()
.into_iter()
.map(|v| (v, max_offence))
.collect::<Vec<_>>();
v16::DisabledValidators::<T>::set(migrated);
log!(info, "v16 applied successfully.");
T::DbWeight::get().reads_writes(1, 1)
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> {
// Decode state to get old_disabled_validators in a format of Vec<u32>
let old_disabled_validators =
Vec::<u32>::decode(&mut state.as_slice()).expect("Failed to decode state");
let new_disabled_validators = v17::DisabledValidators::<T>::get();
// Compare lengths
pezframe_support::ensure!(
old_disabled_validators.len() == new_disabled_validators.len(),
"DisabledValidators length mismatch"
);
// Compare contents
let new_disabled_validators =
new_disabled_validators.into_iter().map(|(v, _)| v).collect::<Vec<_>>();
pezframe_support::ensure!(
old_disabled_validators == new_disabled_validators,
"DisabledValidator ids mismatch"
);
// Verify severity
let max_severity = OffenceSeverity(Perbill::from_percent(100));
let new_disabled_validators = v17::DisabledValidators::<T>::get();
for (_, severity) in new_disabled_validators {
pezframe_support::ensure!(severity == max_severity, "Severity mismatch");
}
Ok(())
}
}
pub type MigrateV15ToV16<T> = VersionedMigration<
15,
16,
VersionUncheckedMigrateV15ToV16<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
}
/// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec<u32>`
pub mod v15 {
use super::*;
// The disabling strategy used by staking pallet
type DefaultDisablingStrategy = pezpallet_session::disabling::UpToLimitDisablingStrategy;
#[storage_alias]
pub(crate) type DisabledValidators<T: Config> = StorageValue<Pallet<T>, Vec<u32>, ValueQuery>;
pub struct VersionUncheckedMigrateV14ToV15<T>(core::marker::PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15<T> {
fn on_runtime_upgrade() -> Weight {
let mut migrated = v14::OffendingValidators::<T>::take()
.into_iter()
.filter(|p| p.1) // take only disabled validators
.map(|p| p.0)
.collect::<Vec<_>>();
// Respect disabling limit
migrated.truncate(DefaultDisablingStrategy::disable_limit(
T::SessionInterface::validators().len(),
));
DisabledValidators::<T>::set(migrated);
log!(info, "v15 applied successfully.");
T::DbWeight::get().reads_writes(1, 1)
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
pezframe_support::ensure!(
v14::OffendingValidators::<T>::decode_len().is_none(),
"OffendingValidators is not empty after the migration"
);
Ok(())
}
}
pub type MigrateV14ToV15<T> = VersionedMigration<
14,
15,
VersionUncheckedMigrateV14ToV15<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
}
/// Migration of era exposure storage items to paged exposures.
/// Changelog: [v14.](https://github.com/paritytech/bizinikiwi/blob/ankan/paged-rewards-rebased2/frame/staking/CHANGELOG.md#14)
pub mod v14 {
use super::*;
#[pezframe_support::storage_alias]
pub(crate) type OffendingValidators<T: Config> =
StorageValue<Pallet<T>, Vec<(u32, bool)>, ValueQuery>;
pub struct MigrateToV14<T>(core::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for MigrateToV14<T> {
fn on_runtime_upgrade() -> Weight {
let in_code = Pallet::<T>::in_code_storage_version();
let on_chain = Pallet::<T>::on_chain_storage_version();
if in_code == 14 && on_chain == 13 {
in_code.put::<Pallet<T>>();
log!(info, "staking v14 applied successfully.");
T::DbWeight::get().reads_writes(1, 1)
} else {
log!(warn, "staking v14 not applied.");
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
pezframe_support::ensure!(
Pallet::<T>::on_chain_storage_version() >= 14,
"v14 not applied"
);
Ok(())
}
}
}
pub mod v13 {
use super::*;
pub struct MigrateToV13<T>(core::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for MigrateToV13<T> {
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
pezframe_support::ensure!(
StorageVersion::<T>::get() == ObsoleteReleases::V12_0_0,
"Required v12 before upgrading to v13"
);
Ok(Default::default())
}
fn on_runtime_upgrade() -> Weight {
let in_code = Pallet::<T>::in_code_storage_version();
let onchain = StorageVersion::<T>::get();
if in_code == 13 && onchain == ObsoleteReleases::V12_0_0 {
StorageVersion::<T>::kill();
in_code.put::<Pallet<T>>();
log!(info, "v13 applied successfully");
T::DbWeight::get().reads_writes(1, 2)
} else {
log!(warn, "Skipping v13, should be removed");
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
pezframe_support::ensure!(
Pallet::<T>::on_chain_storage_version() == 13,
"v13 not applied"
);
pezframe_support::ensure!(
!StorageVersion::<T>::exists(),
"Storage version not migrated correctly"
);
Ok(())
}
}
}
pub mod v12 {
use super::*;
use pezframe_support::{pezpallet_prelude::ValueQuery, storage_alias};
#[storage_alias]
type HistoryDepth<T: Config> = StorageValue<Pallet<T>, u32, ValueQuery>;
/// Clean up `T::HistoryDepth` from storage.
///
/// We will be depending on the configurable value of `T::HistoryDepth` post
/// this release.
pub struct MigrateToV12<T>(core::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for MigrateToV12<T> {
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
pezframe_support::ensure!(
StorageVersion::<T>::get() == ObsoleteReleases::V11_0_0,
"Expected v11 before upgrading to v12"
);
if HistoryDepth::<T>::exists() {
pezframe_support::ensure!(
T::HistoryDepth::get() == HistoryDepth::<T>::get(),
"Provided value of HistoryDepth should be same as the existing storage value"
);
} else {
log::info!("No HistoryDepth in storage; nothing to remove");
}
Ok(Default::default())
}
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
if StorageVersion::<T>::get() == ObsoleteReleases::V11_0_0 {
HistoryDepth::<T>::kill();
StorageVersion::<T>::put(ObsoleteReleases::V12_0_0);
log!(info, "v12 applied successfully");
T::DbWeight::get().reads_writes(1, 2)
} else {
log!(warn, "Skipping v12, should be removed");
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
pezframe_support::ensure!(
StorageVersion::<T>::get() == ObsoleteReleases::V12_0_0,
"v12 not applied"
);
Ok(())
}
}
}
pub mod v11 {
use super::*;
use pezframe_support::{
storage::migration::move_pallet,
traits::{GetStorageVersion, PalletInfoAccess},
};
#[cfg(feature = "try-runtime")]
use pezsp_io::hashing::twox_128;
pub struct MigrateToV11<T, P, N>(core::marker::PhantomData<(T, P, N)>);
impl<T: Config, P: GetStorageVersion + PalletInfoAccess, N: Get<&'static str>> OnRuntimeUpgrade
for MigrateToV11<T, P, N>
{
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
pezframe_support::ensure!(
StorageVersion::<T>::get() == ObsoleteReleases::V10_0_0,
"must upgrade linearly"
);
let old_pallet_prefix = twox_128(N::get().as_bytes());
pezframe_support::ensure!(
pezsp_io::storage::next_key(&old_pallet_prefix).is_some(),
"no data for the old pallet name has been detected"
);
Ok(Default::default())
}
/// Migrate the entire storage of this pallet to a new prefix.
///
/// This new prefix must be the same as the one set in construct_runtime. For safety, use
/// `PalletInfo` to get it, as:
/// `<Runtime as pezframe_system::Config>::PalletInfo::name::<VoterBagsList>`.
///
/// The migration will look into the storage version in order to avoid triggering a
/// migration on an up to date storage.
fn on_runtime_upgrade() -> Weight {
let old_pallet_name = N::get();
let new_pallet_name = <P as PalletInfoAccess>::name();
if StorageVersion::<T>::get() == ObsoleteReleases::V10_0_0 {
// bump version anyway, even if we don't need to move the prefix
StorageVersion::<T>::put(ObsoleteReleases::V11_0_0);
if new_pallet_name == old_pallet_name {
log!(
warn,
"new bags-list name is equal to the old one, only bumping the version"
);
return T::DbWeight::get().reads(1).saturating_add(T::DbWeight::get().writes(1));
}
move_pallet(old_pallet_name.as_bytes(), new_pallet_name.as_bytes());
<T as pezframe_system::Config>::BlockWeights::get().max_block
} else {
log!(warn, "v11::migrate should be removed.");
T::DbWeight::get().reads(1)
}
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
pezframe_support::ensure!(
StorageVersion::<T>::get() == ObsoleteReleases::V11_0_0,
"wrong version after the upgrade"
);
let old_pallet_name = N::get();
let new_pallet_name = <P as PalletInfoAccess>::name();
// skip storage prefix checks for the same pallet names
if new_pallet_name == old_pallet_name {
return Ok(());
}
let old_pallet_prefix = twox_128(N::get().as_bytes());
pezframe_support::ensure!(
pezsp_io::storage::next_key(&old_pallet_prefix).is_none(),
"old pallet data hasn't been removed"
);
let new_pallet_name = <P as PalletInfoAccess>::name();
let new_pallet_prefix = twox_128(new_pallet_name.as_bytes());
pezframe_support::ensure!(
pezsp_io::storage::next_key(&new_pallet_prefix).is_some(),
"new pallet data hasn't been created"
);
Ok(())
}
}
}
+985
View File
@@ -0,0 +1,985 @@
// 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.
//! Test utilities
use crate::{self as pezpallet_staking, *};
use pezframe_election_provider_support::{
bounds::{ElectionBounds, ElectionBoundsBuilder},
onchain, BoundedSupports, SequentialPhragmen, Support, VoteWeight,
};
use pezframe_support::{
assert_ok, derive_impl, ord_parameter_types, parameter_types,
traits::{
ConstU64, EitherOfDiverse, FindAuthor, Get, Imbalance, OnUnbalanced, OneSessionHandler,
RewardsReporter,
},
weights::constants::RocksDbWeight,
};
use pezframe_system::{EnsureRoot, EnsureSignedBy};
use pezsp_core::ConstBool;
use pezsp_io;
use pezsp_runtime::{curve::PiecewiseLinear, testing::UintAuthorityId, traits::Zero, BuildStorage};
use pezsp_staking::{
offence::{OffenceDetails, OnOffenceHandler},
OnStakingUpdate, StakingAccount,
};
pub const INIT_TIMESTAMP: u64 = 30_000;
pub const BLOCK_TIME: u64 = 1000;
pub(crate) const SINGLE_PAGE: u32 = 0;
/// The AccountId alias in this test module.
pub(crate) type AccountId = u64;
pub(crate) type BlockNumber = u64;
pub(crate) type Balance = u128;
/// Another session handler struct to test on_disabled.
pub struct OtherSessionHandler;
impl OneSessionHandler<AccountId> for OtherSessionHandler {
type Key = UintAuthorityId;
fn on_genesis_session<'a, I: 'a>(_: I)
where
I: Iterator<Item = (&'a AccountId, Self::Key)>,
AccountId: 'a,
{
}
fn on_new_session<'a, I: 'a>(_: bool, _: I, _: I)
where
I: Iterator<Item = (&'a AccountId, Self::Key)>,
AccountId: 'a,
{
}
fn on_disabled(_validator_index: u32) {}
}
impl pezsp_runtime::BoundToRuntimeAppPublic for OtherSessionHandler {
type Public = UintAuthorityId;
}
pub fn is_disabled(controller: AccountId) -> bool {
let stash = Ledger::<Test>::get(&controller).unwrap().stash;
let validator_index = match Session::validators().iter().position(|v| *v == stash) {
Some(index) => index as u32,
None => return false,
};
Session::disabled_validators().contains(&validator_index)
}
type Block = pezframe_system::mocking::MockBlock<Test>;
pezframe_support::construct_runtime!(
pub enum Test
{
System: pezframe_system,
Authorship: pezpallet_authorship,
Timestamp: pezpallet_timestamp,
Balances: pezpallet_balances,
Staking: pezpallet_staking,
Session: pezpallet_session,
Historical: pezpallet_session::historical,
VoterBagsList: pezpallet_bags_list::<Instance1>,
}
);
/// Author of block is always 11
pub struct Author11;
impl FindAuthor<AccountId> for Author11 {
fn find_author<'a, I>(_digests: I) -> Option<AccountId>
where
I: 'a + IntoIterator<Item = (pezframe_support::ConsensusEngineId, &'a [u8])>,
{
Some(11)
}
}
parameter_types! {
pub static SessionsPerEra: SessionIndex = 3;
pub static ExistentialDeposit: Balance = 1;
pub static SlashDeferDuration: EraIndex = 0;
pub static Period: BlockNumber = 5;
pub static Offset: BlockNumber = 0;
pub static MaxControllersInDeprecationBatch: u32 = 5900;
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for Test {
type DbWeight = RocksDbWeight;
type Block = Block;
type AccountData = pezpallet_balances::AccountData<Balance>;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for Test {
type MaxLocks = pezframe_support::traits::ConstU32<1024>;
type Balance = Balance;
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
}
pezsp_runtime::impl_opaque_keys! {
pub struct SessionKeys {
pub other: OtherSessionHandler,
}
}
impl pezpallet_session::Config for Test {
type SessionManager = pezpallet_session::historical::NoteHistoricalRoot<Test, Staking>;
type Keys = SessionKeys;
type ShouldEndSession = pezpallet_session::PeriodicSessions<Period, Offset>;
type SessionHandler = (OtherSessionHandler,);
type RuntimeEvent = RuntimeEvent;
type ValidatorId = AccountId;
type ValidatorIdOf = pezsp_runtime::traits::ConvertInto;
type NextSessionRotation = pezpallet_session::PeriodicSessions<Period, Offset>;
type DisablingStrategy =
pezpallet_session::disabling::UpToLimitWithReEnablingDisablingStrategy<DISABLING_LIMIT_FACTOR>;
type WeightInfo = ();
type Currency = Balances;
type KeyDeposit = ();
}
impl pezpallet_session::historical::Config for Test {
type RuntimeEvent = RuntimeEvent;
type FullIdentification = ();
type FullIdentificationOf = crate::UnitIdentificationOf<Self>;
}
impl pezpallet_authorship::Config for Test {
type FindAuthor = Author11;
type EventHandler = ();
}
impl pezpallet_timestamp::Config for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ConstU64<5>;
type WeightInfo = ();
}
pezpallet_staking_reward_curve::build! {
const I_NPOS: PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
parameter_types! {
pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
}
parameter_types! {
pub static RewardRemainderUnbalanced: u128 = 0;
}
pub struct RewardRemainderMock;
impl OnUnbalanced<NegativeImbalanceOf<Test>> for RewardRemainderMock {
fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<Test>) {
RewardRemainderUnbalanced::mutate(|v| {
*v += amount.peek();
});
drop(amount);
}
}
const THRESHOLDS: [pezsp_npos_elections::VoteWeight; 9] =
[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000];
parameter_types! {
pub static BagThresholds: &'static [pezsp_npos_elections::VoteWeight] = &THRESHOLDS;
pub static HistoryDepth: u32 = 80;
pub static MaxExposurePageSize: u32 = 64;
pub static MaxUnlockingChunks: u32 = 32;
pub static RewardOnUnbalanceWasCalled: bool = false;
pub static MaxValidatorSet: u32 = 100;
pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build();
pub static AbsoluteMaxNominations: u32 = 16;
}
type VoterBagsListInstance = pezpallet_bags_list::Instance1;
impl pezpallet_bags_list::Config<VoterBagsListInstance> for Test {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
// Staking is the source of truth for voter bags list, since they are not kept up to date.
type ScoreProvider = Staking;
type BagThresholds = BagThresholds;
type MaxAutoRebagPerBlock = ();
type Score = VoteWeight;
}
parameter_types! {
pub static MaxBackersPerWinner: u32 = 256;
pub static MaxWinnersPerPage: u32 = MaxValidatorSet::get();
}
pub struct OnChainSeqPhragmen;
impl onchain::Config for OnChainSeqPhragmen {
type System = Test;
type Solver = SequentialPhragmen<AccountId, Perbill>;
type DataProvider = Staking;
type WeightInfo = ();
type MaxBackersPerWinner = MaxBackersPerWinner;
type MaxWinnersPerPage = MaxWinnersPerPage;
type Bounds = ElectionsBounds;
type Sort = ConstBool<true>;
}
pub struct MockReward {}
impl OnUnbalanced<PositiveImbalanceOf<Test>> for MockReward {
fn on_unbalanced(_: PositiveImbalanceOf<Test>) {
RewardOnUnbalanceWasCalled::set(true);
}
}
parameter_types! {
pub static LedgerSlashPerEra:
(BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) =
(Zero::zero(), BTreeMap::new());
pub static SlashObserver: BTreeMap<AccountId, BalanceOf<Test>> = BTreeMap::new();
pub static RestrictedAccounts: Vec<AccountId> = Vec::new();
}
pub struct EventListenerMock;
impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
fn on_slash(
pool_account: &AccountId,
slashed_bonded: Balance,
slashed_chunks: &BTreeMap<EraIndex, Balance>,
total_slashed: Balance,
) {
LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
SlashObserver::mutate(|map| {
map.insert(*pool_account, map.get(pool_account).unwrap_or(&0) + total_slashed)
});
}
}
pub struct MockedRestrictList;
impl Contains<AccountId> for MockedRestrictList {
fn contains(who: &AccountId) -> bool {
RestrictedAccounts::get().contains(who)
}
}
// Disabling threshold for `UpToLimitDisablingStrategy` and
// `UpToLimitWithReEnablingDisablingStrategy``
pub(crate) const DISABLING_LIMIT_FACTOR: usize = 3;
#[derive_impl(crate::config_preludes::TestDefaultConfig)]
impl crate::pallet::pallet::Config for Test {
type OldCurrency = Balances;
type Currency = Balances;
type UnixTime = Timestamp;
type RewardRemainder = RewardRemainderMock;
type Reward = MockReward;
type SessionsPerEra = SessionsPerEra;
type SlashDeferDuration = SlashDeferDuration;
type AdminOrigin = EnsureOneOrRoot;
type SessionInterface = Self;
type EraPayout = ConvertCurve<RewardCurve>;
type NextNewSession = Session;
type MaxExposurePageSize = MaxExposurePageSize;
type MaxValidatorSet = MaxValidatorSet;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider;
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
type VoterList = VoterBagsList;
type TargetList = UseValidatorsMap<Self>;
type NominationsQuota = WeightedNominationsQuota<16>;
type MaxUnlockingChunks = MaxUnlockingChunks;
type HistoryDepth = HistoryDepth;
type MaxControllersInDeprecationBatch = MaxControllersInDeprecationBatch;
type EventListeners = EventListenerMock;
type Filter = MockedRestrictList;
}
pub struct WeightedNominationsQuota<const MAX: u32>;
impl<Balance, const MAX: u32> NominationsQuota<Balance> for WeightedNominationsQuota<MAX>
where
u128: From<Balance>,
{
type MaxNominations = AbsoluteMaxNominations;
fn curve(balance: Balance) -> u32 {
match balance.into() {
// random curve for testing.
0..=110 => MAX,
111 => 0,
222 => 2,
333 => MAX + 10,
_ => MAX,
}
}
}
pub(crate) type StakingCall = crate::Call<Test>;
pub(crate) type TestCall = <Test as pezframe_system::Config>::RuntimeCall;
parameter_types! {
// if true, skips the try-state for the test running.
pub static SkipTryStateCheck: bool = false;
}
pub struct ExtBuilder {
nominate: bool,
validator_count: u32,
minimum_validator_count: u32,
invulnerables: Vec<AccountId>,
has_stakers: bool,
initialize_first_session: bool,
pub min_nominator_bond: Balance,
min_validator_bond: Balance,
balance_factor: Balance,
status: BTreeMap<AccountId, StakerStatus<AccountId>>,
stakes: BTreeMap<AccountId, Balance>,
stakers: Vec<(AccountId, AccountId, Balance, StakerStatus<AccountId>)>,
}
impl Default for ExtBuilder {
fn default() -> Self {
Self {
nominate: true,
validator_count: 2,
minimum_validator_count: 0,
balance_factor: 1,
invulnerables: vec![],
has_stakers: true,
initialize_first_session: true,
min_nominator_bond: ExistentialDeposit::get(),
min_validator_bond: ExistentialDeposit::get(),
status: Default::default(),
stakes: Default::default(),
stakers: Default::default(),
}
}
}
impl ExtBuilder {
pub fn existential_deposit(self, existential_deposit: Balance) -> Self {
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = existential_deposit);
self
}
pub fn nominate(mut self, nominate: bool) -> Self {
self.nominate = nominate;
self
}
pub fn validator_count(mut self, count: u32) -> Self {
self.validator_count = count;
self
}
pub fn minimum_validator_count(mut self, count: u32) -> Self {
self.minimum_validator_count = count;
self
}
pub fn slash_defer_duration(self, eras: EraIndex) -> Self {
SLASH_DEFER_DURATION.with(|v| *v.borrow_mut() = eras);
self
}
pub fn invulnerables(mut self, invulnerables: Vec<AccountId>) -> Self {
self.invulnerables = invulnerables;
self
}
pub fn session_per_era(self, length: SessionIndex) -> Self {
SESSIONS_PER_ERA.with(|v| *v.borrow_mut() = length);
self
}
pub fn period(self, length: BlockNumber) -> Self {
PERIOD.with(|v| *v.borrow_mut() = length);
self
}
pub fn has_stakers(mut self, has: bool) -> Self {
self.has_stakers = has;
self
}
pub fn initialize_first_session(mut self, init: bool) -> Self {
self.initialize_first_session = init;
self
}
pub fn offset(self, offset: BlockNumber) -> Self {
OFFSET.with(|v| *v.borrow_mut() = offset);
self
}
pub fn min_nominator_bond(mut self, amount: Balance) -> Self {
self.min_nominator_bond = amount;
self
}
pub fn min_validator_bond(mut self, amount: Balance) -> Self {
self.min_validator_bond = amount;
self
}
pub fn set_status(mut self, who: AccountId, status: StakerStatus<AccountId>) -> Self {
self.status.insert(who, status);
self
}
pub fn set_stake(mut self, who: AccountId, stake: Balance) -> Self {
self.stakes.insert(who, stake);
self
}
pub fn add_staker(
mut self,
stash: AccountId,
ctrl: AccountId,
stake: Balance,
status: StakerStatus<AccountId>,
) -> Self {
self.stakers.push((stash, ctrl, stake, status));
self
}
pub fn balance_factor(mut self, factor: Balance) -> Self {
self.balance_factor = factor;
self
}
pub fn try_state(self, enable: bool) -> Self {
SkipTryStateCheck::set(!enable);
self
}
fn build(self) -> pezsp_io::TestExternalities {
pezsp_tracing::try_init_simple();
let mut storage = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
let ed = ExistentialDeposit::get();
let _ = pezpallet_balances::GenesisConfig::<Test> {
balances: vec![
(1, 10 * self.balance_factor),
(2, 20 * self.balance_factor),
(3, 300 * self.balance_factor),
(4, 400 * self.balance_factor),
// controllers (still used in some tests. Soon to be deprecated).
(10, self.balance_factor),
(20, self.balance_factor),
(30, self.balance_factor),
(40, self.balance_factor),
(50, self.balance_factor),
// stashes
// Note: Previously this pallet used locks and stakers could stake all their
// balance including ED. Now with holds, stakers are required to maintain
// (non-staked) ED in their accounts. Therefore, we drop an additional existential
// deposit to genesis stakers.
(11, self.balance_factor * 1000 + ed),
(21, self.balance_factor * 2000 + ed),
(31, self.balance_factor * 2000 + ed),
(41, self.balance_factor * 2000 + ed),
(51, self.balance_factor * 2000 + ed),
(201, self.balance_factor * 2000 + ed),
(202, self.balance_factor * 2000 + ed),
// optional nominator
(100, self.balance_factor * 2000 + ed),
(101, self.balance_factor * 2000 + ed),
// aux accounts
(60, self.balance_factor),
(61, self.balance_factor * 2000 + ed),
(70, self.balance_factor),
(71, self.balance_factor * 2000),
(80, self.balance_factor),
(81, self.balance_factor * 2000),
// This allows us to have a total_payout different from 0.
(999, 1_000_000_000_000),
],
..Default::default()
}
.assimilate_storage(&mut storage);
let mut stakers = vec![];
if self.has_stakers {
stakers = vec![
// (stash, ctrl, stake, status)
// these two will be elected in the default test where we elect 2.
(11, 11, self.balance_factor * 1000, StakerStatus::<AccountId>::Validator),
(21, 21, self.balance_factor * 1000, StakerStatus::<AccountId>::Validator),
// a loser validator
(31, 31, self.balance_factor * 500, StakerStatus::<AccountId>::Validator),
// an idle validator
(41, 41, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
(51, 51, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
(201, 201, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
(202, 202, self.balance_factor * 1000, StakerStatus::<AccountId>::Idle),
]; // optionally add a nominator
if self.nominate {
stakers.push((
101,
101,
self.balance_factor * 500,
StakerStatus::<AccountId>::Nominator(vec![11, 21]),
))
}
// replace any of the status if needed.
self.status.into_iter().for_each(|(stash, status)| {
let (_, _, _, ref mut prev_status) = stakers
.iter_mut()
.find(|s| s.0 == stash)
.expect("set_status staker should exist; qed");
*prev_status = status;
});
// replaced any of the stakes if needed.
self.stakes.into_iter().for_each(|(stash, stake)| {
let (_, _, ref mut prev_stake, _) = stakers
.iter_mut()
.find(|s| s.0 == stash)
.expect("set_stake staker should exits; qed.");
*prev_stake = stake;
});
// extend stakers if needed.
stakers.extend(self.stakers)
}
let _ = pezpallet_staking::GenesisConfig::<Test> {
stakers: stakers.clone(),
validator_count: self.validator_count,
minimum_validator_count: self.minimum_validator_count,
invulnerables: self.invulnerables,
slash_reward_fraction: Perbill::from_percent(10),
min_nominator_bond: self.min_nominator_bond,
min_validator_bond: self.min_validator_bond,
..Default::default()
}
.assimilate_storage(&mut storage);
let _ = pezpallet_session::GenesisConfig::<Test> {
keys: if self.has_stakers {
// set the keys for the first session.
stakers
.into_iter()
.map(|(id, ..)| (id, id, SessionKeys { other: id.into() }))
.collect()
} else {
// set some dummy validators in genesis.
(0..self.validator_count as u64)
.map(|id| (id, id, SessionKeys { other: id.into() }))
.collect()
},
..Default::default()
}
.assimilate_storage(&mut storage);
let mut ext = pezsp_io::TestExternalities::from(storage);
if self.initialize_first_session {
ext.execute_with(|| {
run_to_block(1);
// Force reset the timestamp to the initial timestamp for easy testing.
Timestamp::set_timestamp(INIT_TIMESTAMP);
});
}
ext
}
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
pezsp_tracing::try_init_simple();
let mut ext = self.build();
ext.execute_with(test);
ext.execute_with(|| {
if !SkipTryStateCheck::get() {
Staking::do_try_state(System::block_number()).unwrap();
}
});
}
}
pub(crate) fn active_era() -> EraIndex {
pezpallet_staking::ActiveEra::<Test>::get().unwrap().index
}
pub(crate) fn current_era() -> EraIndex {
pezpallet_staking::CurrentEra::<Test>::get().unwrap()
}
pub(crate) fn bond(who: AccountId, val: Balance) {
let _ = asset::set_stakeable_balance::<Test>(&who, val);
assert_ok!(Staking::bond(RuntimeOrigin::signed(who), val, RewardDestination::Stash));
}
pub(crate) fn bond_validator(who: AccountId, val: Balance) {
bond(who, val);
assert_ok!(Staking::validate(RuntimeOrigin::signed(who), ValidatorPrefs::default()));
assert_ok!(Session::set_keys(
RuntimeOrigin::signed(who),
SessionKeys { other: who.into() },
vec![]
));
}
pub(crate) fn bond_nominator(who: AccountId, val: Balance, target: Vec<AccountId>) {
bond(who, val);
assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target));
}
pub(crate) fn bond_virtual_nominator(
who: AccountId,
payee: AccountId,
val: Balance,
target: Vec<AccountId>,
) {
// Bond who virtually.
assert_ok!(<Staking as pezsp_staking::StakingUnchecked>::virtual_bond(&who, val, &payee));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target));
}
/// Progress to the given block, triggering session and era changes as we progress.
///
/// This will finalize the previous block, initialize up to the given block, essentially simulating
/// a block import/propose process where we first initialize the block, then execute some stuff (not
/// in the function), and then finalize the block.
pub(crate) fn run_to_block(n: BlockNumber) {
System::run_to_block_with::<AllPalletsWithSystem>(
n,
pezframe_system::RunToBlockHooks::default().after_initialize(|bn| {
Timestamp::set_timestamp(bn * BLOCK_TIME + INIT_TIMESTAMP);
}),
);
}
/// Progresses from the current block number (whatever that may be) to the `P * session_index + 1`.
pub(crate) fn start_session(end_session_idx: SessionIndex) {
let period = Period::get();
let end: u64 = if Offset::get().is_zero() {
(end_session_idx as u64) * period
} else {
Offset::get() + (end_session_idx.saturating_sub(1) as u64) * period
};
run_to_block(end);
let curr_session_idx = Session::current_index();
// session must have progressed properly.
assert_eq!(
curr_session_idx, end_session_idx,
"current session index = {curr_session_idx}, expected = {end_session_idx}",
);
}
/// Go one session forward.
pub(crate) fn advance_session() {
let current_index = Session::current_index();
start_session(current_index + 1);
}
/// Progress until the given era.
pub(crate) fn start_active_era(era_index: EraIndex) {
start_session((era_index * <SessionsPerEra as Get<u32>>::get()).into());
assert_eq!(active_era(), era_index);
// One way or another, current_era must have changed before the active era, so they must match
// at this point.
assert_eq!(current_era(), active_era());
}
pub(crate) fn current_total_payout_for_duration(duration: u64) -> Balance {
let (payout, _rest) = <Test as Config>::EraPayout::era_payout(
pezpallet_staking::ErasTotalStake::<Test>::get(active_era()),
pezpallet_balances::TotalIssuance::<Test>::get(),
duration,
);
assert!(payout > 0);
payout
}
pub(crate) fn maximum_payout_for_duration(duration: u64) -> Balance {
let (payout, rest) = <Test as Config>::EraPayout::era_payout(
pezpallet_staking::ErasTotalStake::<Test>::get(active_era()),
pezpallet_balances::TotalIssuance::<Test>::get(),
duration,
);
payout + rest
}
/// Time it takes to finish a session.
///
/// Note, if you see `time_per_session() - BLOCK_TIME`, it is fine. This is because we set the
/// timestamp after on_initialize, so the timestamp is always one block old.
pub(crate) fn time_per_session() -> u64 {
Period::get() * BLOCK_TIME
}
/// Time it takes to finish an era.
///
/// Note, if you see `time_per_era() - BLOCK_TIME`, it is fine. This is because we set the
/// timestamp after on_initialize, so the timestamp is always one block old.
pub(crate) fn time_per_era() -> u64 {
time_per_session() * SessionsPerEra::get() as u64
}
/// Time that will be calculated for the reward per era.
pub(crate) fn reward_time_per_era() -> u64 {
time_per_era() - BLOCK_TIME
}
pub(crate) fn reward_all_elected() {
let rewards = <Test as Config>::SessionInterface::validators().into_iter().map(|v| (v, 1));
<Pallet<Test>>::reward_by_ids(rewards)
}
pub(crate) fn validator_controllers() -> Vec<AccountId> {
Session::validators()
.into_iter()
.map(|s| Staking::bonded(&s).expect("no controller for validator"))
.collect()
}
pub(crate) fn on_offence_in_era(
offenders: &[OffenceDetails<
AccountId,
pezpallet_session::historical::IdentificationTuple<Test>,
>],
slash_fraction: &[Perbill],
era: EraIndex,
) {
let bonded_eras = crate::BondedEras::<Test>::get();
for &(bonded_era, start_session) in bonded_eras.iter() {
if bonded_era == era {
let _ = <Staking as OnOffenceHandler<_, _, _>>::on_offence(
offenders,
slash_fraction,
start_session,
);
return;
} else if bonded_era > era {
break;
}
}
if pezpallet_staking::ActiveEra::<Test>::get().unwrap().index == era {
let _ = <Staking as OnOffenceHandler<_, _, _>>::on_offence(
offenders,
slash_fraction,
pezpallet_staking::ErasStartSessionIndex::<Test>::get(era).unwrap(),
);
} else {
panic!("cannot slash in era {}", era);
}
}
pub(crate) fn on_offence_now(
offenders: &[OffenceDetails<
AccountId,
pezpallet_session::historical::IdentificationTuple<Test>,
>],
slash_fraction: &[Perbill],
) {
let now = pezpallet_staking::ActiveEra::<Test>::get().unwrap().index;
on_offence_in_era(offenders, slash_fraction, now)
}
pub(crate) fn offence_from(
offender: AccountId,
reporter: Option<Vec<AccountId>>,
) -> OffenceDetails<AccountId, pezpallet_session::historical::IdentificationTuple<Test>> {
OffenceDetails { offender: (offender, ()), reporters: reporter.unwrap_or_default() }
}
pub(crate) fn add_slash(who: &AccountId) {
on_offence_now(&[offence_from(*who, None)], &[Perbill::from_percent(10)]);
}
/// Make all validator and nominator request their payment
pub(crate) fn make_all_reward_payment(era: EraIndex) {
let validators_with_reward = ErasRewardPoints::<Test>::get(era)
.individual
.keys()
.cloned()
.collect::<Vec<_>>();
// reward validators
for validator_controller in validators_with_reward.iter().filter_map(Staking::bonded) {
let ledger = <Ledger<Test>>::get(&validator_controller).unwrap();
for page in 0..EraInfo::<Test>::get_page_count(era, &ledger.stash) {
assert_ok!(Staking::payout_stakers_by_page(
RuntimeOrigin::signed(1337),
ledger.stash,
era,
page
));
}
}
}
pub(crate) fn bond_controller_stash(controller: AccountId, stash: AccountId) -> Result<(), String> {
<Bonded<Test>>::get(&stash).map_or(Ok(()), |_| Err("stash already bonded"))?;
<Ledger<Test>>::get(&controller).map_or(Ok(()), |_| Err("controller already bonded"))?;
<Bonded<Test>>::insert(stash, controller);
<Ledger<Test>>::insert(controller, StakingLedger::<Test>::default_from(stash));
Ok(())
}
// simulates `set_controller` without corrupted ledger checks for testing purposes.
pub(crate) fn set_controller_no_checks(stash: &AccountId) {
let controller = Bonded::<Test>::get(stash).expect("testing stash should be bonded");
let ledger = Ledger::<Test>::get(&controller).expect("testing ledger should exist");
Ledger::<Test>::remove(&controller);
Ledger::<Test>::insert(stash, ledger);
Bonded::<Test>::insert(stash, stash);
}
// simulates `bond_extra` without corrupted ledger checks for testing purposes.
pub(crate) fn bond_extra_no_checks(stash: &AccountId, amount: Balance) {
let controller = Bonded::<Test>::get(stash).expect("bond must exist to bond_extra");
let mut ledger = Ledger::<Test>::get(&controller).expect("ledger must exist to bond_extra");
let new_total = ledger.total + amount;
let _ = asset::update_stake::<Test>(stash, new_total);
ledger.total = new_total;
ledger.active = new_total;
Ledger::<Test>::insert(controller, ledger);
}
pub(crate) fn setup_double_bonded_ledgers() {
let init_ledgers = Ledger::<Test>::iter().count();
let _ = asset::set_stakeable_balance::<Test>(&333, 2000);
let _ = asset::set_stakeable_balance::<Test>(&444, 2000);
let _ = asset::set_stakeable_balance::<Test>(&555, 2000);
let _ = asset::set_stakeable_balance::<Test>(&777, 2000);
assert_ok!(Staking::bond(RuntimeOrigin::signed(333), 10, RewardDestination::Staked));
assert_ok!(Staking::bond(RuntimeOrigin::signed(444), 20, RewardDestination::Staked));
assert_ok!(Staking::bond(RuntimeOrigin::signed(555), 20, RewardDestination::Staked));
// not relevant to the test case, but ensures try-runtime checks pass.
[333, 444, 555]
.iter()
.for_each(|s| Payee::<Test>::insert(s, RewardDestination::Staked));
// we want to test the case where a controller can also be a stash of another ledger.
// for that, we change the controller/stash bonding so that:
// * 444 becomes controller of 333.
// * 555 becomes controller of 444.
// * 777 becomes controller of 555.
let ledger_333 = Ledger::<Test>::get(333).unwrap();
let ledger_444 = Ledger::<Test>::get(444).unwrap();
let ledger_555 = Ledger::<Test>::get(555).unwrap();
// 777 becomes controller of 555.
Bonded::<Test>::mutate(555, |controller| *controller = Some(777));
Ledger::<Test>::insert(777, ledger_555);
// 555 becomes controller of 444.
Bonded::<Test>::mutate(444, |controller| *controller = Some(555));
Ledger::<Test>::insert(555, ledger_444);
// 444 becomes controller of 333.
Bonded::<Test>::mutate(333, |controller| *controller = Some(444));
Ledger::<Test>::insert(444, ledger_333);
// 333 is not controller anymore.
Ledger::<Test>::remove(333);
// checks. now we have:
// * +3 ledgers
assert_eq!(Ledger::<Test>::iter().count(), 3 + init_ledgers);
// * stash 333 has controller 444.
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);
// * stash 444 has controller 555.
assert_eq!(Bonded::<Test>::get(444), Some(555));
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(444)), Some(555));
assert_eq!(Ledger::<Test>::get(555).unwrap().stash, 444);
// * stash 555 has controller 777.
assert_eq!(Bonded::<Test>::get(555), Some(777));
assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(555)), Some(777));
assert_eq!(Ledger::<Test>::get(777).unwrap().stash, 555);
}
#[macro_export]
macro_rules! assert_session_era {
($session:expr, $era:expr) => {
assert_eq!(
Session::current_index(),
$session,
"wrong session {} != {}",
Session::current_index(),
$session,
);
assert_eq!(
CurrentEra::<T>::get().unwrap(),
$era,
"wrong current era {} != {}",
CurrentEra::<T>::get().unwrap(),
$era,
);
};
}
pub(crate) fn staking_events() -> Vec<crate::Event<Test>> {
System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let RuntimeEvent::Staking(inner) = e { Some(inner) } else { None })
.collect()
}
pub(crate) fn session_events() -> Vec<pezpallet_session::Event<Test>> {
System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let RuntimeEvent::Session(inner) = e { Some(inner) } else { None })
.collect()
}
parameter_types! {
static StakingEventsIndex: usize = 0;
}
ord_parameter_types! {
pub const One: u64 = 1;
}
type EnsureOneOrRoot = EitherOfDiverse<EnsureRoot<AccountId>, EnsureSignedBy<One, AccountId>>;
pub(crate) fn staking_events_since_last_call() -> Vec<crate::Event<Test>> {
let all: Vec<_> = System::events()
.into_iter()
.filter_map(|r| if let RuntimeEvent::Staking(inner) = r.event { Some(inner) } else { None })
.collect();
let seen = StakingEventsIndex::get();
StakingEventsIndex::set(all.len());
all.into_iter().skip(seen).collect()
}
pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) {
(asset::stakeable_balance::<Test>(who), Balances::reserved_balance(who))
}
pub(crate) fn restrict(who: &AccountId) {
if !RestrictedAccounts::get().contains(who) {
RestrictedAccounts::mutate(|l| l.push(*who));
}
}
pub(crate) fn remove_from_restrict_list(who: &AccountId) {
RestrictedAccounts::mutate(|l| l.retain(|x| x != who));
}
pub(crate) fn to_bounded_supports(
supports: Vec<(AccountId, Support<AccountId>)>,
) -> BoundedSupports<
AccountId,
<<Test as Config>::ElectionProvider as ElectionProvider>::MaxWinnersPerPage,
<<Test as Config>::ElectionProvider as ElectionProvider>::MaxBackersPerWinner,
> {
supports.try_into().unwrap()
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+821
View File
@@ -0,0 +1,821 @@
// 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.
//!
//! The algorithm implemented in this module tries to balance these 3 difficulties.
//!
//! First, we only slash participants for the _maximum_ slash they receive in some time period,
//! rather than the sum. This ensures a protection from overslashing.
//!
//! Second, we do not want the time period (or "span") that the maximum is computed
//! over to last indefinitely. That would allow participants to begin acting with
//! impunity after some point, fearing no further repercussions. For that reason, we
//! automatically "chill" validators and withdraw a nominator's nomination after a slashing event,
//! requiring them to re-enlist voluntarily (acknowledging the slash) and begin a new
//! slashing span.
//!
//! Typically, you will have a single slashing event per slashing span. Only in the case
//! where a validator releases many misbehaviors at once, or goes "back in time" to misbehave in
//! eras that have already passed, would you encounter situations where a slashing span
//! has multiple misbehaviors. However, accounting for such cases is necessary
//! to deter a class of "rage-quit" attacks.
//!
//! Based on research at <https://research.web3.foundation/en/latest/polkadot/slashing/npos.html>
use crate::{
asset, BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet,
Perbill, SpanSlash, UnappliedSlash, ValidatorSlashInEra,
};
use alloc::vec::Vec;
use codec::{Decode, Encode, MaxEncodedLen};
use pezframe_support::{
ensure,
pezpallet_prelude::DecodeWithMemTracking,
traits::{Defensive, DefensiveSaturating, Imbalance, OnUnbalanced},
};
use scale_info::TypeInfo;
use pezsp_runtime::{
traits::{Saturating, Zero},
DispatchResult, RuntimeDebug,
};
use pezsp_staking::{EraIndex, StakingInterface};
/// The proportion of the slashing reward to be paid out on the first slashing detection.
/// This is f_1 in the paper.
const REWARD_F1: Perbill = Perbill::from_percent(50);
/// The index of a slashing span - unique to each stash.
pub type SpanIndex = u32;
// A range of start..end eras for a slashing span.
#[derive(Encode, Decode, Clone, TypeInfo, RuntimeDebug, PartialEq, Eq)]
pub struct SlashingSpan {
pub index: SpanIndex,
pub start: EraIndex,
pub length: Option<EraIndex>, // the ongoing slashing span has indeterminate length.
}
impl SlashingSpan {
fn contains_era(&self, era: EraIndex) -> bool {
self.start <= era && self.length.map_or(true, |l| self.start.saturating_add(l) > era)
}
}
/// An encoding of all of a nominator's slashing spans.
#[derive(Encode, Decode, Clone, TypeInfo, RuntimeDebug, PartialEq, Eq)]
pub struct SlashingSpans {
// the index of the current slashing span of the nominator. different for
// every stash, resets when the account hits free balance 0.
pub span_index: SpanIndex,
// the start era of the most recent (ongoing) slashing span.
pub last_start: EraIndex,
// the last era at which a non-zero slash occurred.
pub last_nonzero_slash: EraIndex,
// all prior slashing spans' start indices, in reverse order (most recent first)
// encoded as offsets relative to the slashing span after it.
pub prior: Vec<EraIndex>,
}
impl SlashingSpans {
// creates a new record of slashing spans for a stash, starting at the beginning
// of the bonding period, relative to now.
pub(crate) fn new(window_start: EraIndex) -> Self {
SlashingSpans {
span_index: 0,
last_start: window_start,
// initialize to zero, as this structure is lazily created until
// the first slash is applied. setting equal to `window_start` would
// put a time limit on nominations.
last_nonzero_slash: 0,
prior: Vec::new(),
}
}
// update the slashing spans to reflect the start of a new span at the era after `now`
// returns `true` if a new span was started, `false` otherwise. `false` indicates
// that internal state is unchanged.
pub(crate) fn end_span(&mut self, now: EraIndex) -> bool {
let next_start = now.defensive_saturating_add(1);
if next_start <= self.last_start {
return false;
}
let last_length = next_start.defensive_saturating_sub(self.last_start);
self.prior.insert(0, last_length);
self.last_start = next_start;
self.span_index.defensive_saturating_accrue(1);
true
}
// an iterator over all slashing spans in _reverse_ order - most recent first.
pub(crate) fn iter(&'_ self) -> impl Iterator<Item = SlashingSpan> + '_ {
let mut last_start = self.last_start;
let mut index = self.span_index;
let last = SlashingSpan { index, start: last_start, length: None };
let prior = self.prior.iter().cloned().map(move |length| {
let start = last_start.defensive_saturating_sub(length);
last_start = start;
index.defensive_saturating_reduce(1);
SlashingSpan { index, start, length: Some(length) }
});
core::iter::once(last).chain(prior)
}
/// Yields the era index where the most recent non-zero slash occurred.
pub fn last_nonzero_slash(&self) -> EraIndex {
self.last_nonzero_slash
}
// prune the slashing spans against a window, whose start era index is given.
//
// If this returns `Some`, then it includes a range start..end of all the span
// indices which were pruned.
fn prune(&mut self, window_start: EraIndex) -> Option<(SpanIndex, SpanIndex)> {
let old_idx = self
.iter()
.skip(1) // skip ongoing span.
.position(|span| {
span.length
.map_or(false, |len| span.start.defensive_saturating_add(len) <= window_start)
});
let earliest_span_index =
self.span_index.defensive_saturating_sub(self.prior.len() as SpanIndex);
let pruned = match old_idx {
Some(o) => {
self.prior.truncate(o);
let new_earliest =
self.span_index.defensive_saturating_sub(self.prior.len() as SpanIndex);
Some((earliest_span_index, new_earliest))
},
None => None,
};
// readjust the ongoing span, if it started before the beginning of the window.
self.last_start = core::cmp::max(self.last_start, window_start);
pruned
}
}
/// A slashing-span record for a particular stash.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Clone,
Default,
TypeInfo,
MaxEncodedLen,
PartialEq,
Eq,
RuntimeDebug,
)]
pub struct SpanRecord<Balance> {
pub slashed: Balance,
pub paid_out: Balance,
}
impl<Balance> SpanRecord<Balance> {
/// The value of stash balance slashed in this span.
#[cfg(test)]
pub(crate) fn amount(&self) -> &Balance {
&self.slashed
}
}
/// 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 exposure of the stash and all nominators.
pub(crate) exposure: &'a Exposure<T::AccountId, BalanceOf<T>>,
/// The era where the offence occurred.
pub(crate) slash_era: EraIndex,
/// The first era in the current bonding period.
pub(crate) window_start: EraIndex,
/// The current era.
pub(crate) now: EraIndex,
/// The maximum percentage of a slash that ever gets paid out.
/// This is f_inf in the paper.
pub(crate) reward_proportion: Perbill,
}
/// 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.
pub(crate) fn compute_slash<T: Config>(
params: SlashParams<T>,
) -> Option<UnappliedSlash<T::AccountId, BalanceOf<T>>> {
let mut reward_payout = Zero::zero();
let mut val_slashed = Zero::zero();
// is the slash amount here a maximum for the era?
let own_slash = params.slash * params.exposure.own;
if params.slash * params.exposure.total == Zero::zero() {
// kick out the validator even if they won't be slashed,
// as long as the misbehavior is from their most recent slashing span.
kick_out_if_recent::<T>(params);
return None;
}
let prior_slash_p = ValidatorSlashInEra::<T>::get(&params.slash_era, params.stash)
.map_or(Zero::zero(), |(prior_slash_proportion, _)| prior_slash_proportion);
// compare slash proportions rather than slash values to avoid issues due to rounding
// error.
if params.slash.deconstruct() > prior_slash_p.deconstruct() {
ValidatorSlashInEra::<T>::insert(
&params.slash_era,
params.stash,
&(params.slash, own_slash),
);
} else {
// we slash based on the max in era - this new event is not the max,
// so neither the validator or any nominators will need an update.
//
// this does lead to a divergence of our system from the paper, which
// pays out some reward even if the latest report is not max-in-era.
// we opt to avoid the nominator lookups and edits and leave more rewards
// for more drastic misbehavior.
return None;
}
// apply slash to validator.
{
let mut spans = fetch_spans::<T>(
params.stash,
params.window_start,
&mut reward_payout,
&mut val_slashed,
params.reward_proportion,
);
let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash);
if target_span == Some(spans.span_index()) {
// misbehavior occurred within the current slashing span - end current span.
// Check <https://github.com/pezkuwichain/pezkuwi-sdk/issues/124> for details.
spans.end_span(params.now);
}
}
let mut nominators_slashed = Vec::new();
reward_payout += slash_nominators::<T>(params.clone(), prior_slash_p, &mut nominators_slashed);
Some(UnappliedSlash {
validator: params.stash.clone(),
own: val_slashed,
others: nominators_slashed,
reporters: Vec::new(),
payout: reward_payout,
})
}
// doesn't apply any slash, but kicks out the validator if the misbehavior is from
// the most recent slashing span.
fn kick_out_if_recent<T: Config>(params: SlashParams<T>) {
// these are not updated by era-span or end-span.
let mut reward_payout = Zero::zero();
let mut val_slashed = Zero::zero();
let mut spans = fetch_spans::<T>(
params.stash,
params.window_start,
&mut reward_payout,
&mut val_slashed,
params.reward_proportion,
);
if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) {
// Check https://github.com/pezkuwichain/pezkuwi-sdk/issues/124 for details
spans.end_span(params.now);
}
}
/// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
///
/// Returns the amount of reward to pay out.
fn slash_nominators<T: Config>(
params: SlashParams<T>,
prior_slash_p: Perbill,
nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
) -> BalanceOf<T> {
let mut reward_payout = Zero::zero();
nominators_slashed.reserve(params.exposure.others.len());
for nominator in &params.exposure.others {
let stash = &nominator.who;
let mut nom_slashed = Zero::zero();
// the era slash of a nominator always grows, if the validator
// had a new max slash for the era.
let era_slash = {
let own_slash_prior = prior_slash_p * nominator.value;
let own_slash_by_validator = params.slash * nominator.value;
let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior);
let mut era_slash =
NominatorSlashInEra::<T>::get(&params.slash_era, stash).unwrap_or_else(Zero::zero);
era_slash += own_slash_difference;
NominatorSlashInEra::<T>::insert(&params.slash_era, stash, &era_slash);
era_slash
};
// compare the era slash against other eras in the same span.
{
let mut spans = fetch_spans::<T>(
stash,
params.window_start,
&mut reward_payout,
&mut nom_slashed,
params.reward_proportion,
);
let target_span = spans.compare_and_update_span_slash(params.slash_era, era_slash);
if target_span == Some(spans.span_index()) {
// end the span, but don't chill the nominator.
spans.end_span(params.now);
}
}
nominators_slashed.push((stash.clone(), nom_slashed));
}
reward_payout
}
// helper struct for managing a set of spans we are currently inspecting.
// writes alterations to disk on drop, but only if a slash has been carried out.
//
// NOTE: alterations to slashing metadata should not be done after this is dropped.
// dropping this struct applies any necessary slashes, which can lead to free balance
// being 0, and the account being garbage-collected -- a dead account should get no new
// metadata.
struct InspectingSpans<'a, T: Config + 'a> {
dirty: bool,
window_start: EraIndex,
stash: &'a T::AccountId,
spans: SlashingSpans,
paid_out: &'a mut BalanceOf<T>,
slash_of: &'a mut BalanceOf<T>,
reward_proportion: Perbill,
_marker: core::marker::PhantomData<T>,
}
// fetches the slashing spans record for a stash account, initializing it if necessary.
fn fetch_spans<'a, T: Config + 'a>(
stash: &'a T::AccountId,
window_start: EraIndex,
paid_out: &'a mut BalanceOf<T>,
slash_of: &'a mut BalanceOf<T>,
reward_proportion: Perbill,
) -> InspectingSpans<'a, T> {
let spans = crate::SlashingSpans::<T>::get(stash).unwrap_or_else(|| {
let spans = SlashingSpans::new(window_start);
crate::SlashingSpans::<T>::insert(stash, &spans);
spans
});
InspectingSpans {
dirty: false,
window_start,
stash,
spans,
slash_of,
paid_out,
reward_proportion,
_marker: core::marker::PhantomData,
}
}
impl<'a, T: 'a + Config> InspectingSpans<'a, T> {
fn span_index(&self) -> SpanIndex {
self.spans.span_index
}
fn end_span(&mut self, now: EraIndex) {
self.dirty = self.spans.end_span(now) || self.dirty;
}
// add some value to the slash of the staker.
// invariant: the staker is being slashed for non-zero value here
// although `amount` may be zero, as it is only a difference.
fn add_slash(&mut self, amount: BalanceOf<T>, slash_era: EraIndex) {
*self.slash_of += amount;
self.spans.last_nonzero_slash = core::cmp::max(self.spans.last_nonzero_slash, slash_era);
}
// find the span index of the given era, if covered.
fn era_span(&self, era: EraIndex) -> Option<SlashingSpan> {
self.spans.iter().find(|span| span.contains_era(era))
}
// compares the slash in an era to the overall current span slash.
// if it's higher, applies the difference of the slashes and then updates the span on disk.
//
// returns the span index of the era where the slash occurred, if any.
fn compare_and_update_span_slash(
&mut self,
slash_era: EraIndex,
slash: BalanceOf<T>,
) -> Option<SpanIndex> {
let target_span = self.era_span(slash_era)?;
let span_slash_key = (self.stash.clone(), target_span.index);
let mut span_record = SpanSlash::<T>::get(&span_slash_key);
let mut changed = false;
let reward = if span_record.slashed < slash {
// new maximum span slash. apply the difference.
let difference = slash.defensive_saturating_sub(span_record.slashed);
span_record.slashed = slash;
// compute reward.
let reward =
REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out);
self.add_slash(difference, slash_era);
changed = true;
reward
} else if span_record.slashed == slash {
// compute reward. no slash difference to apply.
REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out)
} else {
Zero::zero()
};
if !reward.is_zero() {
changed = true;
span_record.paid_out += reward;
*self.paid_out += reward;
}
if changed {
self.dirty = true;
SpanSlash::<T>::insert(&span_slash_key, &span_record);
}
Some(target_span.index)
}
}
impl<'a, T: 'a + Config> Drop for InspectingSpans<'a, T> {
fn drop(&mut self) {
// only update on disk if we slashed this account.
if !self.dirty {
return;
}
if let Some((start, end)) = self.spans.prune(self.window_start) {
for span_index in start..end {
SpanSlash::<T>::remove(&(self.stash.clone(), span_index));
}
}
crate::SlashingSpans::<T>::insert(self.stash, &self.spans);
}
}
/// 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);
#[allow(deprecated)]
NominatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
}
/// Clear slashing metadata for a dead account.
pub(crate) fn clear_stash_metadata<T: Config>(
stash: &T::AccountId,
num_slashing_spans: u32,
) -> DispatchResult {
let spans = match crate::SlashingSpans::<T>::get(stash) {
None => return Ok(()),
Some(s) => s,
};
ensure!(
num_slashing_spans as usize >= spans.iter().count(),
Error::<T>::IncorrectSlashingSpans
);
crate::SlashingSpans::<T>::remove(stash);
// kill slashing-span metadata for account.
//
// this can only happen while the account is staked _if_ they are completely slashed.
// in that case, they may re-bond, but it would count again as span 0. Further ancient
// slashes would slash into this new bond, since metadata has now been cleared.
for span in spans.iter() {
SpanSlash::<T>::remove(&(stash.clone(), span.index));
}
Ok(())
}
// 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::AccountId, BalanceOf<T>>,
slash_era: EraIndex,
) {
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
let mut reward_payout = unapplied_slash.payout;
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 {
do_slash::<T>(
nominator,
nominator_slash,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
}
/// 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);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_contains_era() {
// unbounded end
let span = SlashingSpan { index: 0, start: 1000, length: None };
assert!(!span.contains_era(0));
assert!(!span.contains_era(999));
assert!(span.contains_era(1000));
assert!(span.contains_era(1001));
assert!(span.contains_era(10000));
// bounded end - non-inclusive range.
let span = SlashingSpan { index: 0, start: 1000, length: Some(10) };
assert!(!span.contains_era(0));
assert!(!span.contains_era(999));
assert!(span.contains_era(1000));
assert!(span.contains_era(1001));
assert!(span.contains_era(1009));
assert!(!span.contains_era(1010));
assert!(!span.contains_era(1011));
}
#[test]
fn single_slashing_span() {
let spans = SlashingSpans {
span_index: 0,
last_start: 1000,
last_nonzero_slash: 0,
prior: Vec::new(),
};
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 0, start: 1000, length: None }],
);
}
#[test]
fn many_prior_spans() {
let spans = SlashingSpans {
span_index: 10,
last_start: 1000,
last_nonzero_slash: 0,
prior: vec![10, 9, 8, 10],
};
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
SlashingSpan { index: 7, start: 973, length: Some(8) },
SlashingSpan { index: 6, start: 963, length: Some(10) },
],
)
}
#[test]
fn pruning_spans() {
let mut spans = SlashingSpans {
span_index: 10,
last_start: 1000,
last_nonzero_slash: 0,
prior: vec![10, 9, 8, 10],
};
assert_eq!(spans.prune(981), Some((6, 8)));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
],
);
assert_eq!(spans.prune(982), None);
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
],
);
assert_eq!(spans.prune(989), None);
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 10, start: 1000, length: None },
SlashingSpan { index: 9, start: 990, length: Some(10) },
SlashingSpan { index: 8, start: 981, length: Some(9) },
],
);
assert_eq!(spans.prune(1000), Some((8, 10)));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 10, start: 1000, length: None },],
);
assert_eq!(spans.prune(2000), None);
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 10, start: 2000, length: None },],
);
// now all in one shot.
let mut spans = SlashingSpans {
span_index: 10,
last_start: 1000,
last_nonzero_slash: 0,
prior: vec![10, 9, 8, 10],
};
assert_eq!(spans.prune(2000), Some((6, 10)));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![SlashingSpan { index: 10, start: 2000, length: None },],
);
}
#[test]
fn ending_span() {
let mut spans = SlashingSpans {
span_index: 1,
last_start: 10,
last_nonzero_slash: 0,
prior: Vec::new(),
};
assert!(spans.end_span(10));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 2, start: 11, length: None },
SlashingSpan { index: 1, start: 10, length: Some(1) },
],
);
assert!(spans.end_span(15));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 3, start: 16, length: None },
SlashingSpan { index: 2, start: 11, length: Some(5) },
SlashingSpan { index: 1, start: 10, length: Some(1) },
],
);
// does nothing if not a valid end.
assert!(!spans.end_span(15));
assert_eq!(
spans.iter().collect::<Vec<_>>(),
vec![
SlashingSpan { index: 3, start: 16, length: None },
SlashingSpan { index: 2, start: 11, length: Some(5) },
SlashingSpan { index: 1, start: 10, length: Some(1) },
],
);
}
}
@@ -0,0 +1,258 @@
// 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);
}
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");
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff