diff --git a/Cargo.lock b/Cargo.lock index ada75934c79b7..fe8a947e0ab0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5372,6 +5372,14 @@ dependencies = [ "syn", ] +[[package]] +name = "pallet-staking-reward-fn" +version = "3.0.0" +dependencies = [ + "log", + "sp-arithmetic", +] + [[package]] name = "pallet-sudo" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index ef2613979518c..742b1f87dbdc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ members = [ "frame/staking", "frame/staking/fuzzer", "frame/staking/reward-curve", + "frame/staking/reward-fn", "frame/sudo", "frame/support", "frame/support/procedural", diff --git a/frame/staking/reward-fn/Cargo.toml b/frame/staking/reward-fn/Cargo.toml new file mode 100644 index 0000000000000..15b17a5e716c3 --- /dev/null +++ b/frame/staking/reward-fn/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pallet-staking-reward-fn" +version = "3.0.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "Reward function for FRAME staking pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] + +[dependencies] +sp-arithmetic = { version = "3.0.0", default-features = false, path = "../../../primitives/arithmetic" } +log = { version = "0.4.14", default-features = false } + +[features] +default = ["std"] +std = [ + "sp-arithmetic/std", + "log/std", +] diff --git a/frame/staking/reward-fn/src/lib.rs b/frame/staking/reward-fn/src/lib.rs new file mode 100644 index 0000000000000..b697842fa69b6 --- /dev/null +++ b/frame/staking/reward-fn/src/lib.rs @@ -0,0 +1,235 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Useful function for inflation for nominated proof of stake. + +use sp_arithmetic::{Perquintill, PerThing, biguint::BigUint, traits::{Zero, SaturatedConversion}}; +use core::convert::TryFrom; + +/// Compute yearly inflation using function +/// +/// ```ignore +/// I(x) = for x between 0 and x_ideal: x / x_ideal, +/// for x between x_ideal and 1: 2^((x_ideal - x) / d) +/// ``` +/// +/// where: +/// * x is the stake rate, i.e. fraction of total issued tokens that actively staked behind +/// validators. +/// * d is the falloff or `decay_rate` +/// * x_ideal: the ideal stake rate. +/// +/// The result is meant to be scaled with minimum inflation and maximum inflation. +/// +/// (as detailed +/// [here](https://research.web3.foundation/en/latest/polkadot/economics/1-token-economics.html#inflation-model-with-parachains)) +/// +/// Arguments are: +/// * `stake`: +/// The fraction of total issued tokens that actively staked behind +/// validators. Known as `x` in the literature. +/// Must be between 0 and 1. +/// * `ideal_stake`: +/// The fraction of total issued tokens that should be actively staked behind +/// validators. Known as `x_ideal` in the literature. +/// Must be between 0 and 1. +/// * `falloff`: +/// Known as `decay_rate` in the literature. A co-efficient dictating the strength of +/// the global incentivization to get the `ideal_stake`. A higher number results in less typical +/// inflation at the cost of greater volatility for validators. +/// Must be more than 0.01. +pub fn compute_inflation( + stake: P, + ideal_stake: P, + falloff: P, +) -> P { + if stake < ideal_stake { + // ideal_stake is more than 0 because it is strictly more than stake + return stake / ideal_stake + } + + if falloff < P::from_percent(1.into()) { + log::error!("Invalid inflation computation: falloff less than 1% is not supported"); + return PerThing::zero() + } + + let accuracy = { + let mut a = BigUint::from(Into::::into(P::ACCURACY)); + a.lstrip(); + a + }; + + let mut falloff = BigUint::from(falloff.deconstruct().into()); + falloff.lstrip(); + + let ln2 = { + let ln2 = P::from_rational(LN2.deconstruct().into(), Perquintill::ACCURACY.into()); + BigUint::from(ln2.deconstruct().into()) + }; + + // falloff is stripped above. + let ln2_div_d = div_by_stripped(ln2.mul(&accuracy), &falloff); + + let inpos_param = INPoSParam { + x_ideal: BigUint::from(ideal_stake.deconstruct().into()), + x: BigUint::from(stake.deconstruct().into()), + accuracy, + ln2_div_d, + }; + + let res = compute_taylor_serie_part(&inpos_param); + + match u128::try_from(res.clone()) { + Ok(res) if res <= Into::::into(P::ACCURACY) => { + P::from_parts(res.saturated_into()) + }, + // If result is beyond bounds there is nothing we can do + _ => { + log::error!("Invalid inflation computation: unexpected result {:?}", res); + P::zero() + }, + } +} + + +/// Internal struct holding parameter info alongside other cached value. +/// +/// All expressed in part from `accuracy` +struct INPoSParam { + ln2_div_d: BigUint, + x_ideal: BigUint, + x: BigUint, + /// Must be stripped and have no leading zeros. + accuracy: BigUint, +} + +/// `ln(2)` expressed in as perquintillionth. +const LN2: Perquintill = Perquintill::from_parts(0_693_147_180_559_945_309); + +/// Compute `2^((x_ideal - x) / d)` using taylor serie. +/// +/// x must be strictly more than x_ideal. +/// +/// result is expressed with accuracy `INPoSParam.accuracy` +fn compute_taylor_serie_part(p: &INPoSParam) -> BigUint { + // The last computed taylor term. + let mut last_taylor_term = p.accuracy.clone(); + + // Whereas taylor sum is positive. + let mut taylor_sum_positive = true; + + // The sum of all taylor term. + let mut taylor_sum = last_taylor_term.clone(); + + for k in 1..300 { + last_taylor_term = compute_taylor_term(k, &last_taylor_term, p); + + if last_taylor_term.is_zero() { + break + } + + let last_taylor_term_positive = k % 2 == 0; + + if taylor_sum_positive == last_taylor_term_positive { + taylor_sum = taylor_sum.add(&last_taylor_term); + } else { + if taylor_sum >= last_taylor_term { + taylor_sum = taylor_sum.sub(&last_taylor_term) + // NOTE: Should never happen as checked above + .unwrap_or_else(|e| e); + } else { + taylor_sum_positive = !taylor_sum_positive; + taylor_sum = last_taylor_term.clone().sub(&taylor_sum) + // NOTE: Should never happen as checked above + .unwrap_or_else(|e| e); + } + } + } + + if !taylor_sum_positive { + return BigUint::zero() + } + + taylor_sum.lstrip(); + taylor_sum +} + +/// Return the absolute value of k-th taylor term of `2^((x_ideal - x))/d` i.e. +/// `((x - x_ideal) * ln(2) / d)^k / k!` +/// +/// x must be strictly more x_ideal. +/// +/// We compute the term from the last term using this formula: +/// +/// `((x - x_ideal) * ln(2) / d)^k / k! == previous_term * (x - x_ideal) * ln(2) / d / k` +/// +/// `previous_taylor_term` and result are expressed with accuracy `INPoSParam.accuracy` +fn compute_taylor_term(k: u32, previous_taylor_term: &BigUint, p: &INPoSParam) -> BigUint { + let x_minus_x_ideal = p.x.clone().sub(&p.x_ideal) + // NOTE: Should never happen, as x must be more than x_ideal + .unwrap_or_else(|_| BigUint::zero()); + + let res = previous_taylor_term.clone() + .mul(&x_minus_x_ideal) + .mul(&p.ln2_div_d) + .div_unit(k); + + // p.accuracy is stripped by definition. + let res = div_by_stripped(res, &p.accuracy); + let mut res = div_by_stripped(res, &p.accuracy); + + res.lstrip(); + res +} + +/// Compute a div b. +/// +/// requires `b` to be stripped and have no leading zeros. +fn div_by_stripped(mut a: BigUint, b: &BigUint) -> BigUint { + a.lstrip(); + + if b.len() == 0 { + log::error!("Computation error: Invalid division"); + return BigUint::zero() + } + + if b.len() == 1 { + return a.div_unit(b.checked_get(0).unwrap_or(1)) + } + + if b.len() > a.len() { + return BigUint::zero() + } + + if b.len() == a.len() { + // 100_000^2 is more than 2^32-1, thus `new_a` has more limbs than `b`. + let mut new_a = a.mul(&BigUint::from(100_000u64.pow(2))); + new_a.lstrip(); + + debug_assert!(new_a.len() > b.len()); + return new_a + .div(b, false) + .map(|res| res.0) + .unwrap_or_else(|| BigUint::zero()) + .div_unit(100_000) + .div_unit(100_000) + } + + a.div(b, false) + .map(|res| res.0) + .unwrap_or_else(|| BigUint::zero()) +} diff --git a/frame/staking/reward-fn/tests/test.rs b/frame/staking/reward-fn/tests/test.rs new file mode 100644 index 0000000000000..32daf9d09a76d --- /dev/null +++ b/frame/staking/reward-fn/tests/test.rs @@ -0,0 +1,101 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sp_arithmetic::{PerThing, Perbill, PerU16, Percent, Perquintill}; + +/// This test the precision and panics if error too big error. +/// +/// error is asserted to be less or equal to 8/accuracy or 8*f64::EPSILON +fn test_precision(stake: P, ideal_stake: P, falloff: P) { + let accuracy_f64 = Into::::into(P::ACCURACY) as f64; + let res = pallet_staking_reward_fn::compute_inflation(stake, ideal_stake, falloff); + let res = Into::::into(res.deconstruct()) as f64 / accuracy_f64; + + let expect = float_i_npos(stake, ideal_stake, falloff); + + let error = (res - expect).abs(); + + if error > 8f64 / accuracy_f64 && error > 8.0 * f64::EPSILON { + panic!( + "stake: {:?}, ideal_stake: {:?}, falloff: {:?}, res: {}, expect: {}", + stake, ideal_stake, falloff, res , expect + ); + } +} + +/// compute the inflation using floats +fn float_i_npos(stake: P, ideal_stake: P, falloff: P) -> f64 { + let accuracy_f64 = Into::::into(P::ACCURACY) as f64; + + let ideal_stake = Into::::into(ideal_stake.deconstruct()) as f64 / accuracy_f64; + let stake = Into::::into(stake.deconstruct()) as f64 / accuracy_f64; + let falloff = Into::::into(falloff.deconstruct()) as f64 / accuracy_f64; + + let x_ideal = ideal_stake; + let x = stake; + let d = falloff; + + if x < x_ideal { + x / x_ideal + } else { + 2_f64.powf((x_ideal - x) / d) + } +} + +#[test] +fn test_precision_for_minimum_falloff() { + fn test_falloff_precision_for_minimum_falloff() { + for stake in 0..1_000 { + let stake = P::from_rational(stake, 1_000); + let ideal_stake = P::zero(); + let falloff = P::from_rational(1, 100); + test_precision(stake, ideal_stake, falloff); + } + } + + test_falloff_precision_for_minimum_falloff::(); + + test_falloff_precision_for_minimum_falloff::(); + + test_falloff_precision_for_minimum_falloff::(); + + test_falloff_precision_for_minimum_falloff::(); +} + +#[test] +fn compute_inflation_works() { + fn compute_inflation_works() { + for stake in 0..100 { + for ideal_stake in 0..10 { + for falloff in 1..10 { + let stake = P::from_rational(stake, 100); + let ideal_stake = P::from_rational(ideal_stake, 10); + let falloff = P::from_rational(falloff, 100); + test_precision(stake, ideal_stake, falloff); + } + } + } + } + + compute_inflation_works::(); + + compute_inflation_works::(); + + compute_inflation_works::(); + + compute_inflation_works::(); +}