mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 14:41:11 +00:00
Make staking inflation curve configurable. (#3644)
* Draft for new design of NPoS rewards * finish code * fix test * add tests * improve log test * version bump * Update srml/staking/reward-curve/Cargo.toml Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * u128 -> u64 * make conversion to smaller type safe * Update core/sr-primitives/src/curve.rs Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
Generated
+26
-4
@@ -2476,6 +2476,7 @@ dependencies = [
|
||||
"srml-offences 1.0.0",
|
||||
"srml-session 2.0.0",
|
||||
"srml-staking 2.0.0",
|
||||
"srml-staking-reward-curve 2.0.0",
|
||||
"srml-sudo 2.0.0",
|
||||
"srml-support 2.0.0",
|
||||
"srml-system 2.0.0",
|
||||
@@ -2599,9 +2600,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -2621,7 +2623,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -4288,6 +4290,7 @@ dependencies = [
|
||||
"srml-authorship 0.1.0",
|
||||
"srml-balances 2.0.0",
|
||||
"srml-session 2.0.0",
|
||||
"srml-staking-reward-curve 2.0.0",
|
||||
"srml-support 2.0.0",
|
||||
"srml-system 2.0.0",
|
||||
"srml-timestamp 2.0.0",
|
||||
@@ -4296,6 +4299,25 @@ dependencies = [
|
||||
"substrate-primitives 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-staking-reward-curve"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sr-primitives 2.0.0",
|
||||
"syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-staking-reward-curve-test"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"sr-primitives 2.0.0",
|
||||
"srml-staking-reward-curve 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-sudo"
|
||||
version = "2.0.0"
|
||||
@@ -4751,7 +4773,7 @@ dependencies = [
|
||||
"futures-timer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"merlin 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -6720,7 +6742,7 @@ dependencies = [
|
||||
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
|
||||
"checksum nohash-hasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0d138afcce92d219ccb6eb53d9b1e8a96ac0d633cfd3c53cd9856d96d1741bb8"
|
||||
"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
|
||||
"checksum num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "57450397855d951f1a41305e54851b1a7b8f5d2e349543a02a2effe25459f718"
|
||||
"checksum num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f9c3f34cdd24f334cb265d9bf8bfa8a241920d026916785747a92f0e55541a1a"
|
||||
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
|
||||
"checksum num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2885278d5fe2adc2f75ced642d52d879bffaceb5a2e0b1d4309ffdfb239b454"
|
||||
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
|
||||
|
||||
@@ -95,6 +95,8 @@ members = [
|
||||
"srml/scored-pool",
|
||||
"srml/session",
|
||||
"srml/staking",
|
||||
"srml/staking/reward-curve",
|
||||
"srml/staking/reward-curve/test",
|
||||
"srml/sudo",
|
||||
"srml/system",
|
||||
"srml/timestamp",
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright 2019 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Provides some utilities to define a piecewise linear function.
|
||||
|
||||
use crate::{Perbill, traits::{SimpleArithmetic, SaturatedConversion}};
|
||||
use core::ops::Sub;
|
||||
|
||||
/// Piecewise Linear function in [0, 1] -> [0, 1].
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct PiecewiseLinear<'a> {
|
||||
/// Array of points. Must be in order from the lowest abscissas to the highest.
|
||||
pub points: &'a [(Perbill, Perbill)]
|
||||
}
|
||||
|
||||
fn abs_sub<N: Ord + Sub<Output=N> + Clone>(a: N, b: N) -> N where {
|
||||
a.clone().max(b.clone()) - a.min(b)
|
||||
}
|
||||
|
||||
impl<'a> PiecewiseLinear<'a> {
|
||||
/// Compute `f(n/d)*d` with `n <= d`. This is useful to avoid loss of precision.
|
||||
pub fn calculate_for_fraction_times_denominator<N>(&self, n: N, d: N) -> N where
|
||||
N: SimpleArithmetic + Clone
|
||||
{
|
||||
let n = n.min(d.clone());
|
||||
|
||||
if self.points.len() == 0 {
|
||||
return N::zero()
|
||||
}
|
||||
|
||||
let next_point_index = self.points.iter()
|
||||
.position(|p| n < p.0 * d.clone());
|
||||
|
||||
let (prev, next) = if let Some(next_point_index) = next_point_index {
|
||||
if let Some(previous_point_index) = next_point_index.checked_sub(1) {
|
||||
(self.points[previous_point_index], self.points[next_point_index])
|
||||
} else {
|
||||
// There is no previous points, take first point ordinate
|
||||
return self.points.first().map(|p| p.1).unwrap_or_else(Perbill::zero) * d
|
||||
}
|
||||
} else {
|
||||
// There is no next points, take last point ordinate
|
||||
return self.points.last().map(|p| p.1).unwrap_or_else(Perbill::zero) * d
|
||||
};
|
||||
|
||||
let delta_y = multiply_by_rational_saturating(
|
||||
abs_sub(n.clone(), prev.0 * d.clone()),
|
||||
abs_sub(next.1.into_parts(), prev.1.into_parts()),
|
||||
// Must not saturate as prev abscissa > next abscissa
|
||||
next.0.into_parts().saturating_sub(prev.0.into_parts()),
|
||||
);
|
||||
|
||||
// If both substration are same sign then result is positive
|
||||
if (n > prev.0 * d.clone()) == (next.1.into_parts() > prev.1.into_parts()) {
|
||||
(prev.1 * d).saturating_add(delta_y)
|
||||
// Otherwise result is negative
|
||||
} else {
|
||||
(prev.1 * d).saturating_sub(delta_y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute value * p / q.
|
||||
// This is guaranteed not to overflow on whatever values nor lose precision.
|
||||
// `q` must be superior to zero.
|
||||
fn multiply_by_rational_saturating<N>(value: N, p: u32, q: u32) -> N
|
||||
where N: SimpleArithmetic + Clone
|
||||
{
|
||||
let q = q.max(1);
|
||||
|
||||
// Mul can saturate if p > q
|
||||
let result_divisor_part = (value.clone() / q.into()).saturating_mul(p.into());
|
||||
|
||||
let result_remainder_part = {
|
||||
let rem = value % q.into();
|
||||
|
||||
// Fits into u32 because q is u32 and remainder < q
|
||||
let rem_u32 = rem.saturated_into::<u32>();
|
||||
|
||||
// Multiplication fits into u64 as both term are u32
|
||||
let rem_part = rem_u32 as u64 * p as u64 / q as u64;
|
||||
|
||||
// Can saturate if p > q
|
||||
rem_part.saturated_into::<N>()
|
||||
};
|
||||
|
||||
// Can saturate if p > q
|
||||
result_divisor_part.saturating_add(result_remainder_part)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiply_by_rational_saturating() {
|
||||
use std::convert::TryInto;
|
||||
|
||||
let div = 100u32;
|
||||
for value in 0..=div {
|
||||
for p in 0..=div {
|
||||
for q in 1..=div {
|
||||
let value: u64 = (value as u128 * u64::max_value() as u128 / div as u128)
|
||||
.try_into().unwrap();
|
||||
let p = (p as u64 * u32::max_value() as u64 / div as u64)
|
||||
.try_into().unwrap();
|
||||
let q = (q as u64 * u32::max_value() as u64 / div as u64)
|
||||
.try_into().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
multiply_by_rational_saturating(value, p, q),
|
||||
(value as u128 * p as u128 / q as u128)
|
||||
.try_into().unwrap_or(u64::max_value())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_for_fraction_times_denominator() {
|
||||
use std::convert::TryInto;
|
||||
|
||||
let curve = PiecewiseLinear {
|
||||
points: &[
|
||||
(Perbill::from_parts(0_000_000_000), Perbill::from_parts(0_500_000_000)),
|
||||
(Perbill::from_parts(0_500_000_000), Perbill::from_parts(1_000_000_000)),
|
||||
(Perbill::from_parts(1_000_000_000), Perbill::from_parts(0_000_000_000)),
|
||||
]
|
||||
};
|
||||
|
||||
pub fn formal_calculate_for_fraction_times_denominator(n: u64, d: u64) -> u64 {
|
||||
if n <= Perbill::from_parts(0_500_000_000) * d.clone() {
|
||||
n + d / 2
|
||||
} else {
|
||||
(d as u128 * 2 - n as u128 * 2).try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
let div = 100u32;
|
||||
for d in 0..=div {
|
||||
for n in 0..=d {
|
||||
let d: u64 = (d as u128 * u64::max_value() as u128 / div as u128)
|
||||
.try_into().unwrap();
|
||||
let n: u64 = (n as u128 * u64::max_value() as u128 / div as u128)
|
||||
.try_into().unwrap();
|
||||
|
||||
let res = curve.calculate_for_fraction_times_denominator(n, d);
|
||||
let expected = formal_calculate_for_fraction_times_denominator(n, d);
|
||||
|
||||
assert!(abs_sub(res, expected) <= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ pub mod testing;
|
||||
|
||||
pub mod weights;
|
||||
pub mod traits;
|
||||
pub mod curve;
|
||||
|
||||
pub mod generic;
|
||||
pub mod transaction_validity;
|
||||
|
||||
@@ -42,6 +42,7 @@ membership = { package = "srml-membership", path = "../../srml/membership", defa
|
||||
offences = { package = "srml-offences", path = "../../srml/offences", default-features = false }
|
||||
session = { package = "srml-session", path = "../../srml/session", default-features = false, features = ["historical"] }
|
||||
staking = { package = "srml-staking", path = "../../srml/staking", default-features = false }
|
||||
srml-staking-reward-curve = { path = "../../srml/staking/reward-curve"}
|
||||
sudo = { package = "srml-sudo", path = "../../srml/sudo", default-features = false }
|
||||
support = { package = "srml-support", path = "../../srml/support", default-features = false }
|
||||
system = { package = "srml-system", path = "../../srml/system", default-features = false }
|
||||
|
||||
@@ -35,7 +35,10 @@ use client::{
|
||||
block_builder::api::{self as block_builder_api, InherentData, CheckInherentsResult},
|
||||
runtime_api as client_api, impl_runtime_apis
|
||||
};
|
||||
use sr_primitives::{ApplyResult, impl_opaque_keys, generic, create_runtime_str, key_types};
|
||||
use sr_primitives::{
|
||||
Permill, Perbill, ApplyResult, impl_opaque_keys, generic, create_runtime_str, key_types
|
||||
};
|
||||
use sr_primitives::curve::PiecewiseLinear;
|
||||
use sr_primitives::transaction_validity::TransactionValidity;
|
||||
use sr_primitives::weights::Weight;
|
||||
use sr_primitives::traits::{
|
||||
@@ -57,7 +60,6 @@ pub use sr_primitives::BuildStorage;
|
||||
pub use timestamp::Call as TimestampCall;
|
||||
pub use balances::Call as BalancesCall;
|
||||
pub use contracts::Gas;
|
||||
pub use sr_primitives::{Permill, Perbill};
|
||||
pub use support::StorageValue;
|
||||
pub use staking::StakerStatus;
|
||||
|
||||
@@ -82,8 +84,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
|
||||
// and set impl_version to equal spec_version. If only runtime
|
||||
// implementation changes and behavior does not, then leave spec_version as
|
||||
// is and increment impl_version.
|
||||
spec_version: 159,
|
||||
impl_version: 159,
|
||||
spec_version: 160,
|
||||
impl_version: 160,
|
||||
apis: RUNTIME_API_VERSIONS,
|
||||
};
|
||||
|
||||
@@ -232,9 +234,21 @@ impl session::historical::Trait for Runtime {
|
||||
type FullIdentificationOf = staking::ExposureOf<Runtime>;
|
||||
}
|
||||
|
||||
srml_staking_reward_curve::build! {
|
||||
const REWARD_CURVE: 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 SessionsPerEra: sr_staking_primitives::SessionIndex = 6;
|
||||
pub const BondingDuration: staking::EraIndex = 24 * 28;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
}
|
||||
|
||||
impl staking::Trait for Runtime {
|
||||
@@ -248,6 +262,7 @@ impl staking::Trait for Runtime {
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SessionInterface = Self;
|
||||
type RewardCurve = RewardCurve;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
|
||||
@@ -23,6 +23,7 @@ authorship = { package = "srml-authorship", path = "../authorship", default-feat
|
||||
primitives = { package = "substrate-primitives", path = "../../core/primitives" }
|
||||
balances = { package = "srml-balances", path = "../balances" }
|
||||
timestamp = { package = "srml-timestamp", path = "../timestamp" }
|
||||
srml-staking-reward-curve = { path = "../staking/reward-curve"}
|
||||
|
||||
[features]
|
||||
equalize = []
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "srml-staking-reward-curve"
|
||||
version = "2.0.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
# sr-api-macros = { path = "../../../core/sr-api-macros" }
|
||||
syn = { version = "1.0", features = [ "full", "visit" ] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
proc-macro-crate = "0.1.3"
|
||||
|
||||
[dev-dependencies]
|
||||
sr-primitives = { path = "../../../core/sr-primitives" }
|
||||
@@ -0,0 +1,414 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
mod log;
|
||||
|
||||
use log::log2;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{TokenStream as TokenStream2, Span};
|
||||
use proc_macro_crate::crate_name;
|
||||
use quote::{quote, ToTokens};
|
||||
use std::convert::TryInto;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
|
||||
/// Accepts a number of expressions to create a instance of PiecewiseLinear which represents the
|
||||
/// NPoS curve (as detailed
|
||||
/// [here](http://research.web3.foundation/en/latest/polkadot/Token%20Economics/#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 incentivisation 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 sr_primitives::curve::PiecewiseLinear;
|
||||
///
|
||||
/// srml_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("sr-primitives") {
|
||||
Ok(sr_primitives) => {
|
||||
let ident = syn::Ident::new(&sr_primitives, Span::call_site());
|
||||
quote!( extern crate #ident as _sr_primitives; )
|
||||
},
|
||||
Err(e) => 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,
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_opposite_after_x_ideal(&self, y: u32) -> u32 {
|
||||
if y == self.i_0 {
|
||||
return u32::max_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![];
|
||||
points.push((0, inpos.i_0));
|
||||
points.push((inpos.x_ideal, inpos.i_ideal_times_x_ideal));
|
||||
|
||||
// For each point p: (next_p.0 - p.0) < segment_lenght && (next_p.1 - p.1) < segment_lenght.
|
||||
// This ensures that the total number of segment 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;
|
||||
|
||||
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 this 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!();
|
||||
|
||||
for (x, y) in points {
|
||||
let error = || panic!(format!(
|
||||
"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!(
|
||||
(
|
||||
_sr_primitives::Perbill::from_const_parts(#x_perbill),
|
||||
_sr_primitives::Perbill::from_const_parts(#y_perbill),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
quote!(
|
||||
_sr_primitives::curve::PiecewiseLinear::<'static> {
|
||||
points: & [ #points_tokens ],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 __srml_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_value()].into_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!(format!("\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."
|
||||
);
|
||||
}
|
||||
}
|
||||
).into()
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Return Per-million value.
|
||||
pub fn log2(p: u32, q: u32) -> u32 {
|
||||
assert!(p >= q);
|
||||
assert!(p <= u32::max_value()/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);
|
||||
|
||||
if p == q {
|
||||
return 0
|
||||
}
|
||||
|
||||
let mut n = 0u32;
|
||||
while !(p >= 2u32.pow(n)*q) || !(p < 2u32.pow(n+1)*q) {
|
||||
n += 1;
|
||||
}
|
||||
assert!(p < 2u32.pow(n+1) * q);
|
||||
|
||||
let y_num: u32 = (p - 2u32.pow(n) * q).try_into().unwrap();
|
||||
let y_den: u32 = (p + 2u32.pow(n) * q).try_into().unwrap();
|
||||
|
||||
let _2_div_ln_2 = 2_885_390u32;
|
||||
|
||||
let taylor_term = |k: u32| -> u32 {
|
||||
if k == 0 {
|
||||
(_2_div_ln_2 as u128 * (y_num as u128).pow(1) / (y_den as u128).pow(1))
|
||||
.try_into().unwrap()
|
||||
} else {
|
||||
let mut res = _2_div_ln_2 as u128 * (y_num as u128).pow(3) / (y_den as u128).pow(3);
|
||||
for _ in 1..k {
|
||||
res = res * (y_num as u128).pow(2) / (y_den as u128).pow(2);
|
||||
}
|
||||
res /= 2 * k as u128 + 1;
|
||||
|
||||
res.try_into().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let mut res = n * 1_000_000u32;
|
||||
let mut k = 0;
|
||||
loop {
|
||||
let term = taylor_term(k);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "srml-staking-reward-curve-test"
|
||||
version = "2.0.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
srml-staking-reward-curve = { path = ".." }
|
||||
sr-primitives = { path = "../../../../core/sr-primitives" }
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Test crate for srml-staking-reward-curve. Allows to test for procedural macro.
|
||||
//! See tests directory.
|
||||
|
||||
mod test_small_falloff {
|
||||
srml_staking_reward_curve::build! {
|
||||
const REWARD_CURVE: sr_primitives::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 {
|
||||
srml_staking_reward_curve::build! {
|
||||
const REWARD_CURVE: sr_primitives::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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,235 +18,71 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! This payout is computed from the desired yearly inflation `I_NPoS`.
|
||||
//!
|
||||
//! `I_NPoS` is defined as such:
|
||||
//!
|
||||
//! let's introduce some constant:
|
||||
//! * `I0` represents a tight upper-bound on our estimate of the operational costs of all
|
||||
//! validators, expressed as a fraction of the total token supply. I_NPoS must be always
|
||||
//! superior or equal to this value.
|
||||
//! * `x_ideal` the ideal staking rate in NPoS.
|
||||
//! * `i_ideal` the ideal yearly interest rate: the ideal total yearly amount of tokens minted to
|
||||
//! pay all validators and nominators for NPoS, divided by the total amount of tokens staked by
|
||||
//! them. `i(x) = I(x)/x` and `i(x_ideal) = i_deal`
|
||||
//! * `d` decay rate.
|
||||
//!
|
||||
//! We define I_NPoS as a linear function from 0 to `x_ideal` and an exponential decrease after
|
||||
//! `x_ideal` to 1. We choose exponential decrease for `I_NPoS` because this implies an exponential
|
||||
//! decrease for the yearly interest rate as well, and we want the interest rate to fall sharply
|
||||
//! beyond `x_ideal` to avoid illiquidity.
|
||||
//!
|
||||
//! Function is defined as such:
|
||||
//! ```nocompile
|
||||
//! for 0 < x < x_ideal: I_NPoS(x) = I0 + x*(i_ideal - I0/x_ideal)
|
||||
//! for x_ideal < x < 1: I_NPoS(x) = I0 + (i_ideal*x_ideal - I0)*2^((x_ideal-x)/d)
|
||||
//! ```
|
||||
//!
|
||||
//! Thus we have the following properties:
|
||||
//! * `I_NPoS > I0`
|
||||
//! * `I_NPoS(0) = I0`
|
||||
//! * `I_NPoS(x_ideal) = max(I_NPoS) = x_ideal*i_ideal`
|
||||
//! * `i(x)` is monotone decreasing
|
||||
//!
|
||||
//! More details can be found [here](http://research.web3.foundation/en/latest/polkadot/Token%20Eco
|
||||
//! nomics/#inflation-model)
|
||||
|
||||
|
||||
use sr_primitives::{Perbill, traits::SimpleArithmetic};
|
||||
|
||||
/// Linear function truncated to positive part `y = max(0, b [+ or -] a*x)` for `P_NPoS` usage.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
struct Linear {
|
||||
// Negate the `a*x` term.
|
||||
negative_a: bool,
|
||||
// Per-billion representation of `a`, the x coefficient.
|
||||
a: u32,
|
||||
// Per-billion representation of `b`, the intercept.
|
||||
b: u32,
|
||||
}
|
||||
|
||||
impl Linear {
|
||||
/// Compute `f(n/d)*d`. This is useful to avoid loss of precision.
|
||||
fn calculate_for_fraction_times_denominator<N>(&self, n: N, d: N) -> N
|
||||
where
|
||||
N: SimpleArithmetic + Clone
|
||||
{
|
||||
if self.negative_a {
|
||||
(Perbill::from_parts(self.b) * d).saturating_sub(Perbill::from_parts(self.a) * n)
|
||||
} else {
|
||||
(Perbill::from_parts(self.b) * d).saturating_add(Perbill::from_parts(self.a) * n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Piecewise Linear function for `P_NPoS` usage
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PiecewiseLinear {
|
||||
/// Array of tuples of an abscissa in a per-billion representation and a linear function.
|
||||
///
|
||||
/// Abscissas in the array must be in order from the lowest to the highest.
|
||||
///
|
||||
/// The array defines a piecewise linear function as such:
|
||||
/// * the n-th segment starts at the abscissa of the n-th element until the abscissa of the
|
||||
/// n-th + 1 element, and is defined by the linear function of the n-th element
|
||||
/// * last segment doesn't end
|
||||
pieces: [(u32, Linear); 20],
|
||||
}
|
||||
|
||||
impl PiecewiseLinear {
|
||||
/// Compute `f(n/d)*d`. This is useful to avoid loss of precision.
|
||||
fn calculate_for_fraction_times_denominator<N>(&self, n: N, d: N) -> N where
|
||||
N: SimpleArithmetic + Clone
|
||||
{
|
||||
let part = self.pieces.iter()
|
||||
.take_while(|(abscissa, _)| n > Perbill::from_parts(*abscissa) * d.clone())
|
||||
.last()
|
||||
.unwrap_or(&self.pieces[0]);
|
||||
|
||||
part.1.calculate_for_fraction_times_denominator(n, d)
|
||||
}
|
||||
}
|
||||
|
||||
/// Piecewise linear approximation of `I_NPoS`.
|
||||
///
|
||||
/// Using the constants:
|
||||
/// * `I_0` = 0.025;
|
||||
/// * `i_ideal` = 0.2;
|
||||
/// * `x_ideal` = 0.5;
|
||||
/// * `d` = 0.05;
|
||||
///
|
||||
/// This approximation is tested to be close to real one by an error less than 1% see
|
||||
/// `i_npos_precision` test.
|
||||
const I_NPOS: PiecewiseLinear = PiecewiseLinear {
|
||||
pieces: [
|
||||
(0, Linear { negative_a: false, a: 150000000, b: 25000000 }),
|
||||
(500000000, Linear { negative_a: true, a: 986493987, b: 593246993 }),
|
||||
(507648979, Linear { negative_a: true, a: 884661327, b: 541551747 }),
|
||||
(515726279, Linear { negative_a: true, a: 788373842, b: 491893761 }),
|
||||
(524282719, Linear { negative_a: true, a: 697631517, b: 444319128 }),
|
||||
(533378749, Linear { negative_a: true, a: 612434341, b: 398876765 }),
|
||||
(543087019, Linear { negative_a: true, a: 532782338, b: 355618796 }),
|
||||
(553495919, Linear { negative_a: true, a: 458675508, b: 314600968 }),
|
||||
(564714479, Linear { negative_a: true, a: 390113843, b: 275883203 }),
|
||||
(576879339, Linear { negative_a: true, a: 327097341, b: 239530285 }),
|
||||
(590164929, Linear { negative_a: true, a: 269626004, b: 205612717 }),
|
||||
(604798839, Linear { negative_a: true, a: 217699848, b: 174207838 }),
|
||||
(621085859, Linear { negative_a: true, a: 171318873, b: 145401271 }),
|
||||
(639447429, Linear { negative_a: true, a: 130483080, b: 119288928 }),
|
||||
(660489879, Linear { negative_a: true, a: 95192479, b: 95979842 }),
|
||||
(685131379, Linear { negative_a: true, a: 65447076, b: 75600334 }),
|
||||
(714860569, Linear { negative_a: true, a: 41246910, b: 58300589 }),
|
||||
(752334749, Linear { negative_a: true, a: 22592084, b: 44265915 }),
|
||||
(803047659, Linear { negative_a: true, a: 9482996, b: 33738693 }),
|
||||
(881691659, Linear { negative_a: true, a: 2572702, b: 27645944 })
|
||||
]
|
||||
};
|
||||
use sr_primitives::{Perbill, traits::SimpleArithmetic, curve::PiecewiseLinear};
|
||||
|
||||
/// The total payout to all validators (and their nominators) per era.
|
||||
///
|
||||
/// Defined as such:
|
||||
/// `payout = yearly_inflation(npos_token_staked / total_tokens) * total_tokans / era_per_year`
|
||||
///
|
||||
/// `era_duration` is expressed in millisecond.
|
||||
///
|
||||
/// Named P_NPoS in the [paper](http://research.web3.foundation/en/latest/polkadot/Token%20Ec
|
||||
/// onomics/#inflation-model).
|
||||
///
|
||||
/// For x the staking rate in NPoS: `P_NPoS(x) = I_NPoS(x) * current_total_token / era_per_year`
|
||||
/// i.e. `P_NPoS(x) = I_NPoS(x) * current_total_token * era_duration / year_duration`
|
||||
///
|
||||
/// I_NPoS is the desired yearly inflation rate for nominated proof of stake.
|
||||
pub fn compute_total_payout<N>(npos_token_staked: N, total_tokens: N, era_duration: u64) -> N where
|
||||
N: SimpleArithmetic + Clone
|
||||
pub fn compute_total_payout<N>(
|
||||
yearly_inflation: &PiecewiseLinear<'static>,
|
||||
npos_token_staked: N,
|
||||
total_tokens: N,
|
||||
era_duration: u64
|
||||
) -> N where N: SimpleArithmetic + Clone
|
||||
{
|
||||
// Milliseconds per year for the Julian year (365.25 days).
|
||||
const MILLISECONDS_PER_YEAR: u64 = 1000 * 3600 * 24 * 36525 / 100;
|
||||
|
||||
Perbill::from_rational_approximation(era_duration as u64, MILLISECONDS_PER_YEAR)
|
||||
* I_NPOS.calculate_for_fraction_times_denominator(npos_token_staked, total_tokens)
|
||||
* yearly_inflation.calculate_for_fraction_times_denominator(npos_token_staked, total_tokens)
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals, non_snake_case)] // To stick with paper notations
|
||||
#[cfg(test)]
|
||||
mod test_inflation {
|
||||
use std::convert::TryInto;
|
||||
mod test {
|
||||
use sr_primitives::curve::PiecewiseLinear;
|
||||
|
||||
// Function `y = a*x + b` using float used for testing precision of Linear
|
||||
#[derive(Debug)]
|
||||
struct LinearFloat {
|
||||
a: f64,
|
||||
b: f64,
|
||||
}
|
||||
|
||||
impl LinearFloat {
|
||||
fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Self {
|
||||
LinearFloat {
|
||||
a: (y1 - y0) / (x1 - x0),
|
||||
b: (x0 * y1 - x1 * y0) / (x0 - x1),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute(&self, x: f64) -> f64 {
|
||||
self.a * x + self.b
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_float_works() {
|
||||
assert_eq!(LinearFloat::new(1.0, 2.0, 4.0, 3.0).compute(7.0), 4.0);
|
||||
}
|
||||
|
||||
// Constants defined in paper
|
||||
const I_0: f64 = 0.025;
|
||||
const i_ideal: f64 = 0.2;
|
||||
const x_ideal: f64 = 0.5;
|
||||
const d: f64 = 0.05;
|
||||
|
||||
// Left part from `x_ideal`
|
||||
fn I_left(x: f64) -> f64 {
|
||||
I_0 + x * (i_ideal - I_0 / x_ideal)
|
||||
}
|
||||
|
||||
// Right part from `x_ideal`
|
||||
fn I_right(x: f64) -> f64 {
|
||||
I_0 + (i_ideal * x_ideal - I_0) * 2_f64.powf((x_ideal - x) / d)
|
||||
}
|
||||
|
||||
// Definition of I_NPoS in float
|
||||
fn I_full(x: f64) -> f64 {
|
||||
if x <= x_ideal {
|
||||
I_left(x)
|
||||
} else {
|
||||
I_right(x)
|
||||
}
|
||||
srml_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;
|
||||
//super::I_NPOS.calculate_for_fraction_times_denominator(25, 100)
|
||||
assert_eq!(super::compute_total_payout(0, 100_000u64, YEAR), 2_498);
|
||||
assert_eq!(super::compute_total_payout(5_000, 100_000u64, YEAR), 3_247);
|
||||
assert_eq!(super::compute_total_payout(25_000, 100_000u64, YEAR), 6_245);
|
||||
assert_eq!(super::compute_total_payout(40_000, 100_000u64, YEAR), 8_494);
|
||||
assert_eq!(super::compute_total_payout(50_000, 100_000u64, YEAR), 9_993);
|
||||
assert_eq!(super::compute_total_payout(60_000, 100_000u64, YEAR), 4_380);
|
||||
assert_eq!(super::compute_total_payout(75_000, 100_000u64, YEAR), 2_735);
|
||||
assert_eq!(super::compute_total_payout(95_000, 100_000u64, YEAR), 2_518);
|
||||
assert_eq!(super::compute_total_payout(100_000, 100_000u64, YEAR), 2_505);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 0, 100_000u64, YEAR), 2_498);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 5_000, 100_000u64, YEAR), 3_247);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 25_000, 100_000u64, YEAR), 6_245);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 40_000, 100_000u64, YEAR), 8_494);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 50_000, 100_000u64, YEAR), 9_993);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 60_000, 100_000u64, YEAR), 4_379);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 75_000, 100_000u64, YEAR), 2_733);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 95_000, 100_000u64, YEAR), 2_513);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 100_000, 100_000u64, YEAR), 2_505);
|
||||
|
||||
const DAY: u64 = 24 * 60 * 60 * 1000;
|
||||
assert_eq!(super::compute_total_payout(25_000, 100_000u64, DAY), 17);
|
||||
assert_eq!(super::compute_total_payout(50_000, 100_000u64, DAY), 27);
|
||||
assert_eq!(super::compute_total_payout(75_000, 100_000u64, DAY), 7);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 25_000, 100_000u64, DAY), 17);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 50_000, 100_000u64, DAY), 27);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 75_000, 100_000u64, DAY), 7);
|
||||
|
||||
const SIX_HOURS: u64 = 6 * 60 * 60 * 1000;
|
||||
assert_eq!(super::compute_total_payout(25_000, 100_000u64, SIX_HOURS), 4);
|
||||
assert_eq!(super::compute_total_payout(50_000, 100_000u64, SIX_HOURS), 6);
|
||||
assert_eq!(super::compute_total_payout(75_000, 100_000u64, SIX_HOURS), 1);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 25_000, 100_000u64, SIX_HOURS), 4);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 50_000, 100_000u64, SIX_HOURS), 6);
|
||||
assert_eq!(super::compute_total_payout(&I_NPOS, 75_000, 100_000u64, SIX_HOURS), 1);
|
||||
|
||||
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
|
||||
@@ -254,167 +90,4 @@ mod test_inflation {
|
||||
57_038_500_000_000_000_000_000
|
||||
);
|
||||
}
|
||||
|
||||
// Compute approximation of I_NPoS into piecewise linear function
|
||||
fn I_NPoS_points() -> super::PiecewiseLinear {
|
||||
let mut points = vec![];
|
||||
|
||||
// Points for left part
|
||||
points.push((0.0, I_0));
|
||||
points.push((x_ideal, I_left(x_ideal)));
|
||||
|
||||
// Approximation for right part.
|
||||
//
|
||||
// We start from x_ideal (x0) and we try to find the next point (x1) for which the linear
|
||||
// approximation of (x0, x1) doesn't deviate from float definition by an error of
|
||||
// GEN_ERROR.
|
||||
|
||||
// When computing deviation between linear approximation and float definition we iterate
|
||||
// over all points with this step precision.
|
||||
const STEP_PRECISION: f64 = 0.000_000_1;
|
||||
// Max error used for generating points.
|
||||
const GEN_ERROR: f64 = 0.000_1;
|
||||
|
||||
let mut x0: f64 = x_ideal;
|
||||
let mut x1: f64 = x0;
|
||||
|
||||
// This is just a step used to find next x1:
|
||||
// if x1 + step result in a not enought precise approximation we reduce step and try again.
|
||||
// we stop as soon as step is less than STEP_PRECISION.
|
||||
let mut step: f64 = 0.1;
|
||||
|
||||
loop {
|
||||
let next_x1 = x1 + step;
|
||||
|
||||
if next_x1 >= 1.0 {
|
||||
points.push((1.0, I_right(1.0)));
|
||||
break;
|
||||
}
|
||||
|
||||
let y0 = I_right(x0);
|
||||
let next_y1 = I_right(next_x1);
|
||||
|
||||
let mut error_overflowed = false;
|
||||
|
||||
// Test error is not overflowed
|
||||
|
||||
// Quick test on one point
|
||||
if (I_right((x0 + next_x1) / 2.0) - (y0 + next_y1) / 2.0).abs() > GEN_ERROR {
|
||||
error_overflowed = true;
|
||||
}
|
||||
|
||||
// Long test on all points
|
||||
if !error_overflowed {
|
||||
let linear = LinearFloat::new(x0, y0, next_x1, next_y1);
|
||||
let mut cursor = x0;
|
||||
while cursor < x1 {
|
||||
if (I_right(cursor) - linear.compute(cursor)).abs() > GEN_ERROR {
|
||||
error_overflowed = true;
|
||||
}
|
||||
cursor += STEP_PRECISION;
|
||||
}
|
||||
}
|
||||
|
||||
if error_overflowed {
|
||||
if step <= STEP_PRECISION {
|
||||
points.push((x1, I_right(x1)));
|
||||
x0 = x1;
|
||||
step = 0.1;
|
||||
} else {
|
||||
step /= 10.0;
|
||||
}
|
||||
} else {
|
||||
x1 = next_x1;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert points to piecewise linear definition
|
||||
let pieces: Vec<(u32, super::Linear)> = (0..points.len()-1)
|
||||
.map(|i| {
|
||||
let p0 = points[i];
|
||||
let p1 = points[i + 1];
|
||||
|
||||
let linear = LinearFloat::new(p0.0, p0.1, p1.0, p1.1);
|
||||
|
||||
// Needed if we want to use a Perbill later
|
||||
assert!(linear.a.abs() <= 1.0);
|
||||
// Needed if we want to use a Perbill later
|
||||
assert!(linear.b.abs() <= 1.0);
|
||||
// Needed to stick with our restrictive definition of linear
|
||||
assert!(linear.b.signum() == 1.0);
|
||||
|
||||
(
|
||||
(p0.0 * 1_000_000_000.0) as u32,
|
||||
super::Linear {
|
||||
negative_a: linear.a.signum() < 0.0,
|
||||
a: (linear.a.abs() * 1_000_000_000.0) as u32,
|
||||
b: (linear.b.abs() * 1_000_000_000.0) as u32,
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("Generated pieces: {:?}", pieces);
|
||||
assert_eq!(pieces.len(), 20);
|
||||
|
||||
super::PiecewiseLinear { pieces: (&pieces[..]).try_into().unwrap() }
|
||||
}
|
||||
|
||||
/// This test is only useful to generate a new set of points for the definition of I_NPoS.
|
||||
#[test]
|
||||
fn generate_I_NPOS() {
|
||||
assert_eq!(super::I_NPOS, I_NPoS_points());
|
||||
}
|
||||
|
||||
/// This test ensure that i_npos piecewise linear approximation is close to the actual function.
|
||||
/// It does compare the result from a computation in integer of different capacity and in f64.
|
||||
#[test]
|
||||
fn i_npos_precision() {
|
||||
const STEP_PRECISION: f64 = 0.000_001;
|
||||
const ERROR: f64 = 0.000_2;
|
||||
|
||||
macro_rules! test_for_value {
|
||||
($($total_token:expr => $type:ty,)*) => {
|
||||
let mut x = 0.1;
|
||||
while x <= 1.0 {
|
||||
let expected = I_full(x);
|
||||
$({
|
||||
let result = super::I_NPOS.calculate_for_fraction_times_denominator(
|
||||
(x * $total_token as f64) as $type,
|
||||
$total_token,
|
||||
) as f64;
|
||||
let expected = expected * $total_token as f64;
|
||||
let error = (ERROR * $total_token as f64).max(2.0);
|
||||
|
||||
let diff = (result - expected).abs();
|
||||
if diff >= error {
|
||||
println!("total_token: {}", $total_token);
|
||||
println!("x: {}", x);
|
||||
println!("diff: {}", diff);
|
||||
println!("error: {}", error);
|
||||
panic!("error overflowed");
|
||||
}
|
||||
})*
|
||||
x += STEP_PRECISION
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_for_value!(
|
||||
1_000u32 => u32,
|
||||
1_000_000u32 => u32,
|
||||
1_000_000_000u32 => u32,
|
||||
1_000_000_000_000u64 => u64,
|
||||
1_000_000_000_000_000u64 => u64,
|
||||
1_000_000_000_000_000_000u64 => u64,
|
||||
1_000_000_000_000_000_000_000u128 => u128,
|
||||
1_000_000_000_000_000_000_000_000u128 => u128,
|
||||
1_000_000_000_000_000_000_000_000_000u128 => u128,
|
||||
1_000_000_000_000_000_000_000_000_000_000u128 => u128,
|
||||
1_000_000_000_000_000_000_000_000_000_000_000_000u128 => u128,
|
||||
u32::max_value() => u32,
|
||||
u64::max_value() => u64,
|
||||
u128::max_value() => u128,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,11 +261,14 @@ use support::{
|
||||
}
|
||||
};
|
||||
use session::{historical::OnSessionEnding, SelectInitialValidators};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_primitives::weights::SimpleDispatchInfo;
|
||||
use sr_primitives::traits::{
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded, SimpleArithmetic,
|
||||
SaturatedConversion,
|
||||
use sr_primitives::{
|
||||
Perbill,
|
||||
curve::PiecewiseLinear,
|
||||
weights::SimpleDispatchInfo,
|
||||
traits::{
|
||||
Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded, SimpleArithmetic,
|
||||
SaturatedConversion,
|
||||
}
|
||||
};
|
||||
use phragmen::{elect, equalize, Support, SupportMap, ExtendedBalance, ACCURACY};
|
||||
use sr_staking_primitives::{
|
||||
@@ -523,6 +526,9 @@ pub trait Trait: system::Trait {
|
||||
|
||||
/// Interface for interacting with a session module.
|
||||
type SessionInterface: self::SessionInterface<Self::AccountId>;
|
||||
|
||||
/// The NPoS reward curve to use.
|
||||
type RewardCurve: Get<&'static PiecewiseLinear<'static>>;
|
||||
}
|
||||
|
||||
/// Mode of era-forcing.
|
||||
@@ -1173,6 +1179,7 @@ impl<T: Trait> Module<T> {
|
||||
let total_rewarded_stake = Self::slot_stake() * validator_len;
|
||||
|
||||
let total_payout = inflation::compute_total_payout(
|
||||
&T::RewardCurve::get(),
|
||||
total_rewarded_stake.clone(),
|
||||
T::Currency::total_issuance(),
|
||||
// Duration of era; more than u64::MAX is rewarded as u64::MAX.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
use std::{collections::HashSet, cell::RefCell};
|
||||
use sr_primitives::Perbill;
|
||||
use sr_primitives::curve::PiecewiseLinear;
|
||||
use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize, SaturatedConversion};
|
||||
use sr_primitives::testing::{Header, UintAuthorityId};
|
||||
use sr_staking_primitives::SessionIndex;
|
||||
@@ -182,9 +183,20 @@ impl timestamp::Trait for Test {
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = MinimumPeriod;
|
||||
}
|
||||
srml_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 SessionsPerEra: SessionIndex = 3;
|
||||
pub const BondingDuration: EraIndex = 3;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
|
||||
}
|
||||
impl Trait for Test {
|
||||
type Currency = balances::Module<Self>;
|
||||
@@ -197,6 +209,7 @@ impl Trait for Test {
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SessionInterface = Self;
|
||||
type RewardCurve = RewardCurve;
|
||||
}
|
||||
|
||||
pub struct ExtBuilder {
|
||||
@@ -430,6 +443,7 @@ pub fn start_era(era_index: EraIndex) {
|
||||
|
||||
pub fn current_total_payout_for_duration(duration: u64) -> u64 {
|
||||
let res = inflation::compute_total_payout(
|
||||
<Test as Trait>::RewardCurve::get(),
|
||||
<Module<Test>>::slot_stake() * 2,
|
||||
Balances::total_issuance(),
|
||||
duration,
|
||||
|
||||
Reference in New Issue
Block a user