diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index c861209f68..c852d480ed 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -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" diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index 6bec85759b..684363c227 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -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", diff --git a/substrate/core/sr-primitives/src/curve.rs b/substrate/core/sr-primitives/src/curve.rs new file mode 100644 index 0000000000..447c57ee32 --- /dev/null +++ b/substrate/core/sr-primitives/src/curve.rs @@ -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 . + +//! 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 + 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(&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(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::(); + + // 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::() + }; + + // 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); + } + } +} diff --git a/substrate/core/sr-primitives/src/lib.rs b/substrate/core/sr-primitives/src/lib.rs index c3f47f29c4..81e73033b2 100644 --- a/substrate/core/sr-primitives/src/lib.rs +++ b/substrate/core/sr-primitives/src/lib.rs @@ -47,6 +47,7 @@ pub mod testing; pub mod weights; pub mod traits; +pub mod curve; pub mod generic; pub mod transaction_validity; diff --git a/substrate/node/runtime/Cargo.toml b/substrate/node/runtime/Cargo.toml index 554d9561a3..8b1f775cea 100644 --- a/substrate/node/runtime/Cargo.toml +++ b/substrate/node/runtime/Cargo.toml @@ -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 } diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs index f6955d2bd0..bee5e7fc22 100644 --- a/substrate/node/runtime/src/lib.rs +++ b/substrate/node/runtime/src/lib.rs @@ -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; } +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! { diff --git a/substrate/srml/staking/Cargo.toml b/substrate/srml/staking/Cargo.toml index 092bcfda8e..3700d84c09 100644 --- a/substrate/srml/staking/Cargo.toml +++ b/substrate/srml/staking/Cargo.toml @@ -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 = [] diff --git a/substrate/srml/staking/reward-curve/Cargo.toml b/substrate/srml/staking/reward-curve/Cargo.toml new file mode 100644 index 0000000000..4e254e9512 --- /dev/null +++ b/substrate/srml/staking/reward-curve/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "srml-staking-reward-curve" +version = "2.0.0" +authors = ["Parity Technologies "] +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" } diff --git a/substrate/srml/staking/reward-curve/src/lib.rs b/substrate/srml/staking/reward-curve/src/lib.rs new file mode 100644 index 0000000000..9ed319be7f --- /dev/null +++ b/substrate/srml/staking/reward-curve/src/lib.rs @@ -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(input: ParseStream, bounds: Bounds) + -> syn::Result +{ + ::parse(&input)?; + ::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 { + let args_input; + + ::parse(&input)?; + let ident = ::parse(&input)?; + ::parse(&input)?; + let typ = ::parse(&input)?; + ::parse(&input)?; + ::parse(&input)?; + ::parse(&input)?; + syn::parenthesized!(args_input in input); + ::parse(&input)?; + + if !input.is_empty() { + return Err(input.error("expected end of input stream, no token expected")); + } + + let min_inflation = parse_field::(&args_input, Bounds { + min: 0, + min_strict: true, + max: 1_000_000, + max_strict: false, + })?; + ::parse(&args_input)?; + let max_inflation = parse_field::(&args_input, Bounds { + min: min_inflation, + min_strict: true, + max: 1_000_000, + max_strict: false, + })?; + ::parse(&args_input)?; + let ideal_stake = parse_field::(&args_input, Bounds { + min: 0_100_000, + min_strict: false, + max: 0_900_000, + max_strict: false, + })?; + ::parse(&args_input)?; + let falloff = parse_field::(&args_input, Bounds { + min: 0_010_000, + min_strict: false, + max: 1_000_000, + max_strict: false, + })?; + ::parse(&args_input)?; + let max_piece_count = parse_field::(&args_input, Bounds { + min: 2, + min_strict: false, + max: 1_000, + max_strict: false, + })?; + ::parse(&args_input)?; + let test_precision = parse_field::(&args_input, Bounds { + min: 0, + min_strict: false, + max: 1_000_000, + max_strict: false, + })?; + >::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() +} diff --git a/substrate/srml/staking/reward-curve/src/log.rs b/substrate/srml/staking/reward-curve/src/log.rs new file mode 100644 index 0000000000..1079591a6c --- /dev/null +++ b/substrate/srml/staking/reward-curve/src/log.rs @@ -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); + } + } +} diff --git a/substrate/srml/staking/reward-curve/test/Cargo.toml b/substrate/srml/staking/reward-curve/test/Cargo.toml new file mode 100644 index 0000000000..5c9ffb3cf7 --- /dev/null +++ b/substrate/srml/staking/reward-curve/test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "srml-staking-reward-curve-test" +version = "2.0.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +srml-staking-reward-curve = { path = ".." } +sr-primitives = { path = "../../../../core/sr-primitives" } diff --git a/substrate/srml/staking/reward-curve/test/src/lib.rs b/substrate/srml/staking/reward-curve/test/src/lib.rs new file mode 100644 index 0000000000..55a3b7d383 --- /dev/null +++ b/substrate/srml/staking/reward-curve/test/src/lib.rs @@ -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 . + +//! 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, + ); + } +} diff --git a/substrate/srml/staking/src/inflation.rs b/substrate/srml/staking/src/inflation.rs index d39e16471b..a1d45b3db1 100644 --- a/substrate/srml/staking/src/inflation.rs +++ b/substrate/srml/staking/src/inflation.rs @@ -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(&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(&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(npos_token_staked: N, total_tokens: N, era_duration: u64) -> N where - N: SimpleArithmetic + Clone +pub fn compute_total_payout( + 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, - ); - } } diff --git a/substrate/srml/staking/src/lib.rs b/substrate/srml/staking/src/lib.rs index 3f88b67875..c450e142af 100644 --- a/substrate/srml/staking/src/lib.rs +++ b/substrate/srml/staking/src/lib.rs @@ -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; + + /// The NPoS reward curve to use. + type RewardCurve: Get<&'static PiecewiseLinear<'static>>; } /// Mode of era-forcing. @@ -1173,6 +1179,7 @@ impl Module { 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. diff --git a/substrate/srml/staking/src/mock.rs b/substrate/srml/staking/src/mock.rs index 747ee64470..ba1645e6d7 100644 --- a/substrate/srml/staking/src/mock.rs +++ b/substrate/srml/staking/src/mock.rs @@ -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; @@ -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( + ::RewardCurve::get(), >::slot_stake() * 2, Balances::total_issuance(), duration,