Rewards & slashes proportional to the least-slashable validator (#781)

* Initial stuff

* Treasury now mints based on lowest staked.

* Tests build

* Fix tests

* Fix hashes

* Fix tests & grumble
This commit is contained in:
Gav Wood
2018-09-21 09:10:16 +02:00
committed by GitHub
parent ebb688f81f
commit 25393abef8
8 changed files with 137 additions and 56 deletions
+57 -2
View File
@@ -95,9 +95,9 @@ pub struct Permill(u32);
// TODO: impl Mul<Permill> for N where N: As<usize>
impl Permill {
pub fn times<N: traits::As<usize> + ::rstd::ops::Mul<N, Output=N> + ::rstd::ops::Div<N, Output=N>>(self, b: N) -> N {
pub fn times<N: traits::As<u64> + ::rstd::ops::Mul<N, Output=N> + ::rstd::ops::Div<N, Output=N>>(self, b: N) -> N {
// TODO: handle overflows
b * <N as traits::As<usize>>::sa(self.0 as usize) / <N as traits::As<usize>>::sa(1000000)
b * <N as traits::As<u64>>::sa(self.0 as u64) / <N as traits::As<u64>>::sa(1000000)
}
pub fn from_millionths(x: u32) -> Permill { Permill(x) }
@@ -122,6 +122,61 @@ impl From<f32> for Permill {
}
}
/// Perbill is parts-per-billion. It stores a value between 0 and 1 in fixed point and
/// provides a means to multiply some other value by that.
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
#[derive(Encode, Decode, Default, Copy, Clone, PartialEq, Eq)]
pub struct Perbill(u32);
// TODO: impl Mul<Perbill> for N where N: As<usize>
impl Perbill {
/// Attenuate `b` by self.
pub fn times<N: traits::As<u64> + ::rstd::ops::Mul<N, Output=N> + ::rstd::ops::Div<N, Output=N>>(self, b: N) -> N {
// TODO: handle overflows
b * <N as traits::As<u64>>::sa(self.0 as u64) / <N as traits::As<u64>>::sa(1_000_000_000)
}
/// Nothing.
pub fn zero() -> Perbill { Perbill(0) }
/// `true` if this is nothing.
pub fn is_zero(&self) -> bool { self.0 == 0 }
/// Everything.
pub fn one() -> Perbill { Perbill(1_000_000_000) }
/// Construct new instance where `x` is in billionths. Value equivalent to `x / 1,000,000,000`.
pub fn from_billionths(x: u32) -> Perbill { Perbill(x.min(1_000_000_000)) }
/// Construct new instance where `x` is in millionths. Value equivalent to `x / 1,000,000`.
pub fn from_millionths(x: u32) -> Perbill { Perbill(x.min(1_000_000) * 1000) }
/// Construct new instance where `x` is a percent. Value equivalent to `x%`.
pub fn from_percent(x: u32) -> Perbill { Perbill(x.min(100) * 10_000_000) }
#[cfg(feature = "std")]
/// Construct new instance whose value is equal to `x` (between 0 and 1).
pub fn from_fraction(x: f64) -> Perbill { Perbill((x.max(0.0).min(1.0) * 1_000_000_000.0) as u32) }
#[cfg(feature = "std")]
/// Construct new instance whose value is equal to `n / d` (between 0 and 1).
pub fn from_rational(n: f64, d: f64) -> Perbill { Perbill(((n / d).max(0.0).min(1.0) * 1_000_000_000.0) as u32) }
}
#[cfg(feature = "std")]
impl From<f64> for Perbill {
fn from(x: f64) -> Perbill {
Perbill::from_fraction(x)
}
}
#[cfg(feature = "std")]
impl From<f32> for Perbill {
fn from(x: f32) -> Perbill {
Perbill::from_fraction(x as f64)
}
}
/// Ed25519 signature verify.
#[derive(Eq, PartialEq, Clone, Default, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))]
+9 -7
View File
@@ -54,7 +54,7 @@ mod tests {
ed25519::{Public, Pair}};
use node_primitives::{Hash, BlockNumber, AccountId};
use runtime_primitives::traits::{Header as HeaderT, Digest as DigestT};
use runtime_primitives::{generic, generic::Era, ApplyOutcome, ApplyError, ApplyResult};
use runtime_primitives::{generic, generic::Era, ApplyOutcome, ApplyError, ApplyResult, Perbill};
use {balances, staking, session, system, consensus, timestamp, treasury};
use system::{EventRecord, Phase};
use node_runtime::{Header, Block, UncheckedExtrinsic, CheckedExtrinsic, Call, Runtime, Balances,
@@ -238,8 +238,10 @@ mod tests {
validator_count: 3,
minimum_validator_count: 0,
bonding_duration: 0,
offline_slash: 0,
session_reward: 0,
offline_slash: Perbill::zero(),
session_reward: Perbill::zero(),
current_offline_slash: 0,
current_session_reward: 0,
offline_slash_grace: 0,
}),
democracy: Some(Default::default()),
@@ -284,9 +286,9 @@ mod tests {
1,
GENESIS_HASH.into(),
if support_changes_trie {
hex!("1755be7303767b4d3855694b4f0ebd9d64b7011124d0ec1ad3e17c2a0d65e245").into()
hex!("6ed28a339a836a106e244dd78a2952606169cdeef3f72e29fa61c6ab827626c1").into()
} else {
hex!("1f058f699ad3187bcf7e9ed8e44464d7a5added0cd912d2679b9dab2e7a04053").into()
hex!("fc51c67ef0f8b2b91db40e6d8757e12ee670b9775479312f3d25bab66b1bec35").into()
},
if support_changes_trie {
Some(hex!("d7ff76d7fbb9b613e8d140da6f1d561b4928785d4e4818ed959bd1bd35abc7e8").into())
@@ -310,7 +312,7 @@ mod tests {
construct_block(
2,
block1(false).1,
hex!("0acf8b3c169ce8f16faf5610f646f371681dcc3b544d3dd05036dbae7890e399").into(),
hex!("4611bee90608d504e0e66773391361a1f9084adbb15732b82206f381d27d64d0").into(),
None,
vec![
CheckedExtrinsic {
@@ -333,7 +335,7 @@ mod tests {
construct_block(
1,
GENESIS_HASH.into(),
hex!("fe0e07c7b054fe186387461d455d536860e9c71d6979fd9dbf755e96ce070d04").into(),
hex!("681893f1698410a9b378ff7b69c7a0d8f7cc417a274583f6217d041b84c3cbae").into(),
None,
vec![
CheckedExtrinsic {
+1 -1
View File
@@ -74,7 +74,7 @@ use council::{motions as council_motions, voting as council_voting};
pub use runtime_primitives::BuildStorage;
pub use consensus::Call as ConsensusCall;
pub use timestamp::Call as TimestampCall;
pub use runtime_primitives::Permill;
pub use runtime_primitives::{Permill, Perbill};
#[cfg(any(feature = "std", test))]
pub use checked_block::CheckedBlock;
+9 -5
View File
@@ -19,7 +19,7 @@
use primitives::{AuthorityId, ed25519};
use node_runtime::{GenesisConfig, ConsensusConfig, CouncilConfig, DemocracyConfig,
SessionConfig, StakingConfig, TimestampConfig, BalancesConfig, TreasuryConfig,
ContractConfig, Permill};
ContractConfig, Permill, Perbill};
use service::ChainSpec;
const STAGING_TELEMETRY_URL: &str = "wss://telemetry.polkadot.io/submit/";
@@ -61,8 +61,10 @@ fn staging_testnet_config_genesis() -> GenesisConfig {
staking: Some(StakingConfig {
current_era: 0,
intentions: initial_authorities.iter().cloned().map(Into::into).collect(),
offline_slash: 10000,
session_reward: 100,
offline_slash: Perbill::from_millionths(1000),
session_reward: Perbill::from_billionths(60),
current_offline_slash: 0,
current_session_reward: 0,
validator_count: 12,
sessions_per_era: 12, // 1 hour per era
bonding_duration: 24 * 60 * 12, // 1 day per bond.
@@ -156,8 +158,10 @@ fn testnet_genesis(initial_authorities: Vec<AuthorityId>) -> GenesisConfig {
validator_count: 2,
sessions_per_era: 5,
bonding_duration: 2 * 60 * 12,
offline_slash: 0,
session_reward: 0,
offline_slash: Perbill::zero(),
session_reward: Perbill::zero(),
current_offline_slash: 0,
current_session_reward: 0,
offline_slash_grace: 0,
}),
democracy: Some(DemocracyConfig {
+13 -6
View File
@@ -22,9 +22,10 @@ use rstd::prelude::*;
use codec::Encode;
use runtime_support::StorageValue;
use primitives::traits::As;
use primitives;
use primitives::{self, Perbill};
use super::{Trait, Intentions, CurrentEra, OfflineSlashGrace, MinimumValidatorCount,
BondingDuration, SessionsPerEra, ValidatorCount, SessionReward, OfflineSlash};
BondingDuration, SessionsPerEra, ValidatorCount, SessionReward, OfflineSlash,
CurrentSessionReward, CurrentOfflineSlash};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -36,8 +37,10 @@ pub struct GenesisConfig<T: Trait> {
pub validator_count: u32,
pub minimum_validator_count: u32,
pub bonding_duration: T::BlockNumber,
pub session_reward: T::Balance,
pub offline_slash: T::Balance,
pub session_reward: Perbill,
pub offline_slash: Perbill,
pub current_session_reward: T::Balance,
pub current_offline_slash: T::Balance,
pub offline_slash_grace: u32,
}
@@ -50,8 +53,10 @@ impl<T: Trait> Default for GenesisConfig<T> {
validator_count: 0,
minimum_validator_count: 0,
bonding_duration: T::BlockNumber::sa(1000),
session_reward: T::Balance::sa(0),
offline_slash: T::Balance::sa(0),
session_reward: Perbill::from_billionths(60),
offline_slash: Perbill::from_fraction(0.001),
current_session_reward: T::Balance::sa(0),
current_offline_slash: T::Balance::sa(0),
offline_slash_grace: 0,
}
}
@@ -68,6 +73,8 @@ impl<T: Trait> primitives::BuildStorage for GenesisConfig<T> {
Self::hash(<CurrentEra<T>>::key()).to_vec() => self.current_era.encode(),
Self::hash(<SessionReward<T>>::key()).to_vec() => self.session_reward.encode(),
Self::hash(<OfflineSlash<T>>::key()).to_vec() => self.offline_slash.encode(),
Self::hash(<CurrentSessionReward<T>>::key()).to_vec() => self.current_session_reward.encode(),
Self::hash(<CurrentOfflineSlash<T>>::key()).to_vec() => self.current_offline_slash.encode(),
Self::hash(<OfflineSlashGrace<T>>::key()).to_vec() => self.offline_slash_grace.encode()
])
}
+24 -14
View File
@@ -52,7 +52,7 @@ use rstd::cmp;
use runtime_support::{Parameter, StorageValue, StorageMap};
use runtime_support::dispatch::Result;
use session::OnSessionChange;
use primitives::traits::{Zero, One, Bounded, OnFinalise, As};
use primitives::{Perbill, traits::{Zero, One, Bounded, OnFinalise, As}};
use balances::{address::Address, OnDilution};
use system::ensure_signed;
@@ -143,9 +143,9 @@ decl_storage! {
/// The length of a staking era in sessions.
pub SessionsPerEra get(sessions_per_era): required T::BlockNumber;
/// Maximum reward, per validator, that is provided per acceptable session.
pub SessionReward get(session_reward): required T::Balance;
pub SessionReward get(session_reward): required Perbill;
/// Slash, per validator that is taken for the first time they are found to be offline.
pub OfflineSlash get(offline_slash): required T::Balance;
pub OfflineSlash get(offline_slash): required Perbill;
/// Number of instances of offline reports before slashing begins for validators.
pub OfflineSlashGrace get(offline_slash_grace): default u32;
/// The length of the bonding duration in blocks.
@@ -163,6 +163,12 @@ decl_storage! {
pub NominatorsFor get(nominators_for): default map [ T::AccountId => Vec<T::AccountId> ];
/// Nominators for a particular account that is in action right now.
pub CurrentNominatorsFor get(current_nominators_for): default map [ T::AccountId => Vec<T::AccountId> ];
/// Maximum reward, per validator, that is provided per acceptable session.
pub CurrentSessionReward get(current_session_reward): default T::Balance;
/// Slash, per validator that is taken for the first time they are found to be offline.
pub CurrentOfflineSlash get(current_offline_slash): default T::Balance;
/// The next value of sessions per era.
pub NextSessionsPerEra get(next_sessions_per_era): T::BlockNumber;
/// The session index at which the era length last changed.
@@ -423,7 +429,7 @@ impl<T: Trait> Module<T> {
fn this_session_reward(actual_elapsed: T::Moment) -> T::Balance {
let ideal_elapsed = <session::Module<T>>::ideal_session_duration();
let per65536: u64 = (T::Moment::sa(65536u64) * ideal_elapsed.clone() / actual_elapsed.max(ideal_elapsed)).as_();
Self::session_reward() * T::Balance::sa(per65536) / T::Balance::sa(65536u64)
Self::current_session_reward() * T::Balance::sa(per65536) / T::Balance::sa(65536u64)
}
/// Session has just changed. We need to determine whether we pay a reward, slash and/or
@@ -438,7 +444,7 @@ impl<T: Trait> Module<T> {
}
Self::deposit_event(RawEvent::Reward(reward));
let total_minted = reward * <T::Balance as As<usize>>::sa(validators.len());
let total_rewarded_stake = Self::stake_range().0 * <T::Balance as As<usize>>::sa(validators.len());
let total_rewarded_stake = Self::stake_range().1 * <T::Balance as As<usize>>::sa(validators.len());
T::OnRewardMinted::on_dilution(total_minted, total_rewarded_stake);
}
@@ -485,14 +491,14 @@ impl<T: Trait> Module<T> {
intentions.sort_unstable_by(|&(ref b1, _), &(ref b2, _)| b2.cmp(&b1));
let desired_validator_count = <ValidatorCount<T>>::get() as usize;
<StakeRange<T>>::put(
if !intentions.is_empty() {
let n = cmp::min(desired_validator_count, intentions.len());
(intentions[0].0, intentions[n - 1].0)
} else {
(Zero::zero(), Zero::zero())
}
);
let stake_range = if !intentions.is_empty() {
let n = cmp::min(desired_validator_count, intentions.len());
(intentions[0].0, intentions[n - 1].0)
} else {
(Zero::zero(), Zero::zero())
};
<StakeRange<T>>::put(&stake_range);
let vals = &intentions.into_iter()
.map(|(_, v)| v)
.take(desired_validator_count)
@@ -508,6 +514,10 @@ impl<T: Trait> Module<T> {
<CurrentNominatorsFor<T>>::insert(v, Self::nominators_for(v));
}
<session::Module<T>>::set_validators(vals);
// Update the balances for slashing/rewarding according to the stakes.
<CurrentOfflineSlash<T>>::put(Self::offline_slash().times(stake_range.1));
<CurrentSessionReward<T>>::put(Self::session_reward().times(stake_range.1));
}
}
@@ -547,7 +557,7 @@ impl<T: Trait> consensus::OnOfflineValidator for Module<T> {
let event = if slash_count >= grace {
let instances = slash_count - grace;
let slash = Self::offline_slash() << instances;
let slash = Self::current_offline_slash() << instances;
let next_slash = slash << 1u32;
let _ = Self::slash_validator(&v, slash);
if instances >= Self::validator_preferences(&v).unstake_threshold
+5 -3
View File
@@ -19,7 +19,7 @@
#![cfg(test)]
use primitives::BuildStorage;
use primitives::traits::{Identity};
use primitives::{Perbill, traits::Identity};
use primitives::testing::{Digest, DigestItem, Header};
use substrate_primitives::{H256, Blake2Hasher, RlpCodec};
use runtime_io;
@@ -117,8 +117,10 @@ pub fn new_test_ext(
validator_count: 2,
minimum_validator_count: 0,
bonding_duration: sessions_per_era * session_length * 3,
session_reward: reward,
offline_slash: if monied { 20 } else { 0 },
session_reward: Perbill::from_millionths((1000000 * reward / balance_factor) as u32),
offline_slash: if monied { Perbill::from_percent(40) } else { Perbill::zero() },
current_session_reward: reward,
current_offline_slash: 20,
offline_slash_grace: 0,
}.build_storage().unwrap());
t.extend(timestamp::GenesisConfig::<Test>{
+19 -18
View File
@@ -316,10 +316,10 @@ fn nominating_and_rewards_should_work() {
Session::check_rotate_session(System::block_number());
assert_eq!(Staking::current_era(), 2);
assert_eq!(Session::validators(), vec![3, 2]);
assert_eq!(Balances::total_balance(&1), 12);
assert_eq!(Balances::total_balance(&1), 16);
assert_eq!(Balances::total_balance(&2), 20);
assert_eq!(Balances::total_balance(&3), 40);
assert_eq!(Balances::total_balance(&4), 48);
assert_eq!(Balances::total_balance(&3), 60);
assert_eq!(Balances::total_balance(&4), 64);
System::set_block_number(3);
assert_ok!(Staking::stake(Origin::signed(4)));
@@ -327,17 +327,17 @@ fn nominating_and_rewards_should_work() {
assert_ok!(Staking::nominate(Origin::signed(3), 1.into()));
Session::check_rotate_session(System::block_number());
assert_eq!(Session::validators(), vec![1, 4]);
assert_eq!(Balances::total_balance(&1), 12);
assert_eq!(Balances::total_balance(&2), 30);
assert_eq!(Balances::total_balance(&3), 50);
assert_eq!(Balances::total_balance(&4), 48);
assert_eq!(Balances::total_balance(&1), 16);
assert_eq!(Balances::total_balance(&2), 40);
assert_eq!(Balances::total_balance(&3), 80);
assert_eq!(Balances::total_balance(&4), 64);
System::set_block_number(4);
Session::check_rotate_session(System::block_number());
assert_eq!(Balances::total_balance(&1), 13);
assert_eq!(Balances::total_balance(&2), 30);
assert_eq!(Balances::total_balance(&3), 58);
assert_eq!(Balances::total_balance(&4), 58);
assert_eq!(Balances::total_balance(&1), 26);
assert_eq!(Balances::total_balance(&2), 40);
assert_eq!(Balances::total_balance(&3), 133);
assert_eq!(Balances::total_balance(&4), 128);
});
}
@@ -357,9 +357,9 @@ fn rewards_with_off_the_table_should_work() {
System::set_block_number(2);
assert_ok!(Staking::register_preferences(Origin::signed(1), Staking::intentions().into_iter().position(|i| i == 1).unwrap() as u32, ValidatorPrefs { unstake_threshold: 3, validator_payment: 4 }));
Session::check_rotate_session(System::block_number());
assert_eq!(Balances::total_balance(&1), 16);
assert_eq!(Balances::total_balance(&2), 24);
assert_eq!(Balances::total_balance(&3), 40);
assert_eq!(Balances::total_balance(&1), 22);
assert_eq!(Balances::total_balance(&2), 37);
assert_eq!(Balances::total_balance(&3), 60);
});
}
@@ -393,10 +393,11 @@ fn nominating_slashes_should_work() {
System::set_extrinsic_index(1);
Staking::on_offline_validator(0);
Staking::on_offline_validator(1);
assert_eq!(Balances::total_balance(&1), 0);
assert_eq!(Balances::total_balance(&2), 20);
assert_eq!(Balances::total_balance(&3), 10);
assert_eq!(Balances::total_balance(&4), 30);
assert_eq!(Balances::total_balance(&1), 0); //slashed
assert_eq!(Balances::total_balance(&2), 20); //not slashed
assert_eq!(Balances::total_balance(&3), 10); //slashed
assert_eq!(Balances::total_balance(&4), 30); //slashed
// TODO: change slash % to something sensible.
});
}