New slashing logic (#570)

* New slashing mechanism (#554)

* Slashing improvements

- unstake when balance too low
- unstake after N slashes according to val prefs
- don't early-terminate session/era unless unstaked
- offline grace period before punishment

* Fix warning

* Cleanups and ensure slash_count decays

* Bump authoring version and introduce needed authoring stub

* Rename

* Fix offline tracker

* Fix offline tracker

* Renames

* Add test

* Tests

* Tests.

* Remove accidental merge files.

* Version bump, fixes (#572)

* Bump version, don't propose invalid blocks

* Fix build.

* Fixes.

* More fixes.

* Fix tests.

* Fix more tests

* More tests fixed

* Fix merge

* Fix accidental merge bug

* Fixes.

* Staking failsafes

- Don't slash/unstake/change session when too few staking participants
- Introduce set_balance PrivCall

* Make minimum validator count dynamic.

* test fixes

* Fix tests.

* Fix tests

* Fix tests, update readme.

* Test with release.

* Use safe math when dealing with total stake

* Fix test again.

* Fix grumbles.
This commit is contained in:
Gav Wood
2018-08-27 17:11:04 +02:00
committed by GitHub
parent 4e9f40840c
commit f59bcd15da
18 changed files with 490 additions and 191 deletions
@@ -55,7 +55,7 @@ impl staking::Trait for Test {
type OnAccountKill = Contract;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = Staking;
}
@@ -100,6 +100,7 @@ fn new_test_ext(existential_deposit: u64, gas_price: u64) -> runtime_io::TestExt
balances: vec![],
intentions: vec![],
validator_count: 2,
minimum_validator_count: 0,
bonding_duration: 0,
transaction_base_fee: 0,
transaction_byte_fee: 0,
@@ -109,6 +110,7 @@ fn new_test_ext(existential_deposit: u64, gas_price: u64) -> runtime_io::TestExt
reclaim_rebate: 0,
early_era_slash: 0,
session_reward: 0,
offline_slash_grace: 0,
}.build_storage()
.unwrap(),
);
@@ -653,7 +653,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = staking::Module<Test>;
}
@@ -688,6 +688,7 @@ mod tests {
balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)],
intentions: vec![],
validator_count: 2,
minimum_validator_count: 0,
bonding_duration: 0,
transaction_base_fee: 0,
transaction_byte_fee: 0,
@@ -697,6 +698,7 @@ mod tests {
reclaim_rebate: 0,
early_era_slash: 0,
session_reward: 0,
offline_slash_grace: 0,
}.build_storage().unwrap());
t.extend(democracy::GenesisConfig::<Test>{
launch_period: 1,
@@ -395,7 +395,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = staking::Module<Test>;
}
@@ -429,6 +429,7 @@ mod tests {
balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)],
intentions: vec![],
validator_count: 2,
minimum_validator_count: 0,
bonding_duration: 3,
transaction_base_fee: 0,
transaction_byte_fee: 0,
@@ -438,6 +439,7 @@ mod tests {
reclaim_rebate: 0,
early_era_slash: 0,
session_reward: 0,
offline_slash_grace: 0,
}.build_storage().unwrap());
t.extend(GenesisConfig::<Test>{
launch_period: 1,
@@ -499,7 +501,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (10, 0));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 2);
});
@@ -577,19 +579,19 @@ mod tests {
System::set_block_number(1);
assert_ok!(Democracy::vote(&1, 0, true));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::bonding_duration(), 4);
System::set_block_number(2);
assert_ok!(Democracy::vote(&1, 1, true));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::bonding_duration(), 3);
System::set_block_number(3);
assert_ok!(Democracy::vote(&1, 2, true));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::bonding_duration(), 2);
});
}
@@ -610,7 +612,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (10, 0));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 2);
});
@@ -625,7 +627,7 @@ mod tests {
assert_ok!(Democracy::cancel_referendum(r));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 1);
});
@@ -643,7 +645,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (0, 10));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 1);
});
@@ -664,7 +666,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (110, 100));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 2);
});
@@ -681,7 +683,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (60, 50));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 1);
});
@@ -702,7 +704,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (100, 50));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(0, Vec::new());
Staking::on_session_change(0, true);
assert_eq!(Staking::era_length(), 2);
});
@@ -252,7 +252,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = staking::Module<Test>;
}
@@ -278,6 +278,7 @@ mod tests {
balances: vec![(1, 111)],
intentions: vec![],
validator_count: 0,
minimum_validator_count: 0,
bonding_duration: 0,
transaction_base_fee: 10,
transaction_byte_fee: 0,
@@ -287,6 +288,7 @@ mod tests {
reclaim_rebate: 0,
early_era_slash: 0,
session_reward: 0,
offline_slash_grace: 0,
}.build_storage().unwrap());
let xt = primitives::testing::TestXt((1, 0, Call::transfer(2.into(), 69)));
let mut t = runtime_io::TestExternalities::from(t);
@@ -317,8 +319,8 @@ mod tests {
// Blake
// state_root: hex!("02532989c613369596025dfcfc821339fc9861987003924913a5a1382f87034a").into(),
// Keccak
state_root: hex!("8fad93b6b9e5251a2e4913598fd0d74a138c0e486eb1133ff8081b429b0c56f2").into(),
extrinsics_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").into(), // REVIEW: I expected this to be wrong with a different hasher?
state_root: hex!("ed456461b82664990b6ebd1caf1360056f6e8a062e73fada331e1c92cd81cad4").into(),
extrinsics_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").into(),
digest: Digest { logs: vec![], },
},
extrinsics: vec![],
@@ -351,7 +353,7 @@ mod tests {
header: Header {
parent_hash: [69u8; 32].into(),
number: 1,
state_root: hex!("8fad93b6b9e5251a2e4913598fd0d74a138c0e486eb1133ff8081b429b0c56f2").into(),
state_root: hex!("ed456461b82664990b6ebd1caf1360056f6e8a062e73fada331e1c92cd81cad4").into(),
extrinsics_root: [0u8; 32].into(),
digest: Digest { logs: vec![], },
},
@@ -26,7 +26,8 @@ use codec::{Codec, Encode};
pub use integer_sqrt::IntegerSquareRoot;
pub use num_traits::{Zero, One, Bounded};
pub use num_traits::ops::checked::{CheckedAdd, CheckedSub, CheckedMul, CheckedDiv};
use rstd::ops::{Add, Sub, Mul, Div, Rem, AddAssign, SubAssign, MulAssign, DivAssign, RemAssign};
use rstd::ops::{Add, Sub, Mul, Div, Rem, AddAssign, SubAssign, MulAssign, DivAssign,
RemAssign, Shl, Shr};
/// A lazy value.
pub trait Lazy<T: ?Sized> {
@@ -132,6 +133,7 @@ pub trait SimpleArithmetic:
Mul<Self, Output = Self> + MulAssign<Self> +
Div<Self, Output = Self> + DivAssign<Self> +
Rem<Self, Output = Self> + RemAssign<Self> +
Shl<u32, Output = Self> + Shr<u32, Output = Self> +
CheckedAdd +
CheckedSub +
CheckedMul +
@@ -145,6 +147,7 @@ impl<T:
Mul<Self, Output = Self> + MulAssign<Self> +
Div<Self, Output = Self> + DivAssign<Self> +
Rem<Self, Output = Self> + RemAssign<Self> +
Shl<u32, Output = Self> + Shr<u32, Output = Self> +
CheckedAdd +
CheckedSub +
CheckedMul +
@@ -34,4 +34,5 @@ std = [
"substrate-runtime-primitives/std",
"substrate-runtime-consensus/std",
"substrate-runtime-system/std",
"substrate-runtime-timestamp/std"
]
+19 -47
View File
@@ -54,21 +54,21 @@ use runtime_support::dispatch::Result;
use std::collections::HashMap;
/// A session has changed.
pub trait OnSessionChange<T, A> {
pub trait OnSessionChange<T> {
/// Session has changed.
fn on_session_change(time_elapsed: T, bad_validators: Vec<A>);
fn on_session_change(time_elapsed: T, should_reward: bool);
}
impl<T, A> OnSessionChange<T, A> for () {
fn on_session_change(_: T, _: Vec<A>) {}
impl<T> OnSessionChange<T> for () {
fn on_session_change(_: T, _: bool) {}
}
pub trait Trait: timestamp::Trait {
// the position of the required timestamp-set extrinsic.
const NOTE_OFFLINE_POSITION: u32;
// the position of the required note_missed_proposal extrinsic.
const NOTE_MISSED_PROPOSAL_POSITION: u32;
type ConvertAccountIdToSessionKey: Convert<Self::AccountId, Self::SessionKey>;
type OnSessionChange: OnSessionChange<Self::Moment, Self::AccountId>;
type OnSessionChange: OnSessionChange<Self::Moment>;
}
decl_module! {
@@ -83,7 +83,7 @@ decl_module! {
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum PrivCall {
fn set_length(new: T::BlockNumber) -> Result = 0;
fn force_new_session(normal_rotation: bool) -> Result = 1;
fn force_new_session(apply_rewards: bool) -> Result = 1;
}
}
@@ -142,8 +142,8 @@ impl<T: Trait> Module<T> {
}
/// Forces a new session.
pub fn force_new_session(normal_rotation: bool) -> Result {
<ForcingNewSession<T>>::put(normal_rotation);
pub fn force_new_session(apply_rewards: bool) -> Result {
<ForcingNewSession<T>>::put(apply_rewards);
Ok(())
}
@@ -151,9 +151,9 @@ impl<T: Trait> Module<T> {
pub fn note_offline(aux: &T::PublicAux, offline_val_indices: Vec<u32>) -> Result {
assert!(aux.is_empty());
assert!(
<system::Module<T>>::extrinsic_index() == T::NOTE_OFFLINE_POSITION,
"note_offline extrinsic must be at position {} in the block",
T::NOTE_OFFLINE_POSITION
<system::Module<T>>::extrinsic_index() == T::NOTE_MISSED_PROPOSAL_POSITION,
"note_missed_proposal extrinsic must be at position {} in the block",
T::NOTE_MISSED_PROPOSAL_POSITION
);
let vs = Self::validators();
@@ -181,15 +181,15 @@ impl<T: Trait> Module<T> {
// check block number and call next_session if necessary.
let block_number = <system::Module<T>>::block_number();
let is_final_block = ((block_number - Self::last_length_change()) % Self::length()).is_zero();
let bad_validators = <BadValidators<T>>::take().unwrap_or_default();
let should_end_session = <ForcingNewSession<T>>::take().is_some() || !bad_validators.is_empty() || is_final_block;
let (should_end_session, apply_rewards) = <ForcingNewSession<T>>::take()
.map_or((is_final_block, is_final_block), |apply_rewards| (true, apply_rewards));
if should_end_session {
Self::rotate_session(is_final_block, bad_validators);
Self::rotate_session(is_final_block, apply_rewards);
}
}
/// Move onto next session: register the new authority set.
pub fn rotate_session(is_final_block: bool, bad_validators: Vec<T::AccountId>) {
pub fn rotate_session(is_final_block: bool, apply_rewards: bool) {
let now = <timestamp::Module<T>>::get();
let time_elapsed = now.clone() - Self::current_start();
@@ -209,7 +209,7 @@ impl<T: Trait> Module<T> {
<LastLengthChange<T>>::put(block_number);
}
T::OnSessionChange::on_session_change(time_elapsed, bad_validators);
T::OnSessionChange::on_session_change(time_elapsed, apply_rewards);
// Update any changes in session keys.
Self::validators().iter().enumerate().for_each(|(i, v)| {
@@ -314,7 +314,7 @@ mod tests {
type Moment = u64;
}
impl Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = ();
}
@@ -350,34 +350,6 @@ mod tests {
});
}
#[test]
fn should_rotate_on_bad_validators() {
with_externalities(&mut new_test_ext(), || {
System::set_block_number(2);
assert_eq!(Session::blocks_remaining(), 0);
Timestamp::set_timestamp(0);
assert_ok!(Session::set_length(3));
Session::check_rotate_session();
assert_eq!(Session::current_index(), 1);
assert_eq!(Session::length(), 3);
assert_eq!(Session::current_start(), 0);
assert_eq!(Session::ideal_session_duration(), 15);
// ideal end = 0 + 15 * 3 = 15
System::set_block_number(3);
assert_eq!(Session::blocks_remaining(), 2);
Timestamp::set_timestamp(9); // no bad validators. session not rotated.
Session::check_rotate_session();
System::set_block_number(4);
::system::ExtrinsicIndex::<Test>::put(1);
assert_eq!(Session::blocks_remaining(), 1);
Session::note_offline(&0, vec![1]).unwrap(); // bad validator -> session rotate
Session::check_rotate_session();
assert_eq!(Session::current_index(), 2);
});
}
#[test]
fn should_work_with_early_exit() {
with_externalities(&mut new_test_ext(), || {
@@ -10,6 +10,7 @@ serde_derive = { version = "1.0", optional = true }
safe-mix = { version = "1.0", default_features = false}
substrate-keyring = { path = "../../keyring", optional = true }
substrate-codec = { path = "../../codec", default_features = false }
substrate-codec-derive = { path = "../../codec/derive", default_features = false }
substrate-primitives = { path = "../../primitives", default_features = false }
substrate-runtime-std = { path = "../../runtime-std", default_features = false }
substrate-runtime-io = { path = "../../runtime-io", default_features = false }
@@ -32,6 +33,7 @@ std = [
"safe-mix/std",
"substrate-keyring",
"substrate-codec/std",
"substrate-codec-derive/std",
"substrate-primitives/std",
"substrate-runtime-std/std",
"substrate-runtime-io/std",
@@ -40,4 +42,5 @@ std = [
"substrate-runtime-primitives/std",
"substrate-runtime-session/std",
"substrate-runtime-system/std",
"substrate-runtime-timestamp/std"
]
@@ -28,7 +28,8 @@ use {runtime_io, primitives};
use super::{Trait, ENUM_SET_SIZE, EnumSet, NextEnumSet, Intentions, CurrentEra,
BondingDuration, CreationFee, TransferFee, ReclaimRebate,
ExistentialDeposit, TransactionByteFee, TransactionBaseFee, TotalStake,
SessionsPerEra, ValidatorCount, FreeBalance, SessionReward, EarlyEraSlash};
SessionsPerEra, ValidatorCount, FreeBalance, SessionReward, EarlyEraSlash,
OfflineSlashGrace, MinimumValidatorCount};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -38,7 +39,8 @@ pub struct GenesisConfig<T: Trait> {
pub current_era: T::BlockNumber,
pub balances: Vec<(T::AccountId, T::Balance)>,
pub intentions: Vec<T::AccountId>,
pub validator_count: u64,
pub validator_count: u32,
pub minimum_validator_count: u32,
pub bonding_duration: T::BlockNumber,
pub transaction_base_fee: T::Balance,
pub transaction_byte_fee: T::Balance,
@@ -48,6 +50,7 @@ pub struct GenesisConfig<T: Trait> {
pub existential_deposit: T::Balance,
pub session_reward: T::Balance,
pub early_era_slash: T::Balance,
pub offline_slash_grace: u32,
}
impl<T: Trait> GenesisConfig<T> where T::AccountId: From<u64> {
@@ -58,6 +61,7 @@ impl<T: Trait> GenesisConfig<T> where T::AccountId: From<u64> {
balances: vec![(T::AccountId::from(1), T::Balance::sa(111))],
intentions: vec![T::AccountId::from(1), T::AccountId::from(2), T::AccountId::from(3)],
validator_count: 3,
minimum_validator_count: 1,
bonding_duration: T::BlockNumber::sa(0),
transaction_base_fee: T::Balance::sa(0),
transaction_byte_fee: T::Balance::sa(0),
@@ -67,6 +71,7 @@ impl<T: Trait> GenesisConfig<T> where T::AccountId: From<u64> {
reclaim_rebate: T::Balance::sa(0),
session_reward: T::Balance::sa(0),
early_era_slash: T::Balance::sa(0),
offline_slash_grace: 1,
}
}
@@ -85,6 +90,7 @@ impl<T: Trait> GenesisConfig<T> where T::AccountId: From<u64> {
],
intentions: vec![T::AccountId::from(1), T::AccountId::from(2), T::AccountId::from(3)],
validator_count: 3,
minimum_validator_count: 1,
bonding_duration: T::BlockNumber::sa(0),
transaction_base_fee: T::Balance::sa(1),
transaction_byte_fee: T::Balance::sa(0),
@@ -94,6 +100,7 @@ impl<T: Trait> GenesisConfig<T> where T::AccountId: From<u64> {
reclaim_rebate: T::Balance::sa(0),
session_reward: T::Balance::sa(0),
early_era_slash: T::Balance::sa(0),
offline_slash_grace: 1,
}
}
}
@@ -106,6 +113,7 @@ impl<T: Trait> Default for GenesisConfig<T> {
balances: vec![],
intentions: vec![],
validator_count: 0,
minimum_validator_count: 0,
bonding_duration: T::BlockNumber::sa(1000),
transaction_base_fee: T::Balance::sa(0),
transaction_byte_fee: T::Balance::sa(0),
@@ -115,6 +123,7 @@ impl<T: Trait> Default for GenesisConfig<T> {
reclaim_rebate: T::Balance::sa(0),
session_reward: T::Balance::sa(0),
early_era_slash: T::Balance::sa(0),
offline_slash_grace: 0,
}
}
}
@@ -128,6 +137,7 @@ impl<T: Trait> primitives::BuildStorage for GenesisConfig<T> {
Self::hash(<Intentions<T>>::key()).to_vec() => self.intentions.encode(),
Self::hash(<SessionsPerEra<T>>::key()).to_vec() => self.sessions_per_era.encode(),
Self::hash(<ValidatorCount<T>>::key()).to_vec() => self.validator_count.encode(),
Self::hash(<MinimumValidatorCount<T>>::key()).to_vec() => self.minimum_validator_count.encode(),
Self::hash(<BondingDuration<T>>::key()).to_vec() => self.bonding_duration.encode(),
Self::hash(<TransactionBaseFee<T>>::key()).to_vec() => self.transaction_base_fee.encode(),
Self::hash(<TransactionByteFee<T>>::key()).to_vec() => self.transaction_byte_fee.encode(),
@@ -138,6 +148,7 @@ 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(<EarlyEraSlash<T>>::key()).to_vec() => self.early_era_slash.encode(),
Self::hash(<OfflineSlashGrace<T>>::key()).to_vec() => self.offline_slash_grace.encode(),
Self::hash(<TotalStake<T>>::key()).to_vec() => total_stake.encode()
];
+248 -102
View File
@@ -34,6 +34,9 @@ extern crate substrate_runtime_support as runtime_support;
#[cfg_attr(feature = "std", macro_use)]
extern crate substrate_runtime_std as rstd;
#[macro_use]
extern crate substrate_codec_derive;
extern crate substrate_codec as codec;
extern crate substrate_primitives;
extern crate substrate_runtime_io as runtime_io;
@@ -52,7 +55,7 @@ use runtime_support::{StorageValue, StorageMap, Parameter};
use runtime_support::dispatch::Result;
use session::OnSessionChange;
use primitives::traits::{Zero, One, Bounded, RefInto, SimpleArithmetic, Executable, MakePayment,
As, AuxLookup, Member, CheckedAdd, CheckedSub};
As, AuxLookup, Member, CheckedAdd, CheckedSub, MaybeEmpty};
use address::Address as RawAddress;
mod mock;
@@ -64,6 +67,8 @@ mod genesis_config;
#[cfg(feature = "std")]
pub use genesis_config::GenesisConfig;
const DEFAULT_MINIMUM_VALIDATOR_COUNT: usize = 4;
/// Number of account IDs stored per enum set.
const ENUM_SET_SIZE: usize = 64;
@@ -98,7 +103,26 @@ impl<AccountId> OnAccountKill<AccountId> for () {
fn on_account_kill(_who: &AccountId) {}
}
/// Preference of what happens on a slash event.
#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))]
#[derive(Encode, Decode, Eq, PartialEq, Clone, Copy)]
pub struct SlashPreference {
/// Validator should ensure this many more slashes than is necessary before being unstaked.
pub unstake_threshold: u32,
}
impl Default for SlashPreference {
fn default() -> Self {
SlashPreference {
unstake_threshold: 3,
}
}
}
pub trait Trait: system::Trait + session::Trait {
/// The allowed extrinsic position for `missed_proposal` inherent.
// const NOTE_MISSED_PROPOSAL_POSITION: u32; // TODO: uncomment when removed from session::Trait
/// The balance of an account.
type Balance: Parameter + SimpleArithmetic + Codec + Default + Copy + As<Self::AccountIndex> + As<usize> + As<u64>;
/// Type used for storing an account's index; implies the maximum number of accounts the system
@@ -117,9 +141,11 @@ decl_module! {
pub enum Call where aux: T::PublicAux {
fn transfer(aux, dest: RawAddress<T::AccountId, T::AccountIndex>, value: T::Balance) -> Result = 0;
fn stake(aux) -> Result = 1;
fn unstake(aux, index: u32) -> Result = 2;
fn unstake(aux, intentions_index: u32) -> Result = 2;
fn nominate(aux, target: RawAddress<T::AccountId, T::AccountIndex>) -> Result = 3;
fn unnominate(aux, target_index: u32) -> Result = 4;
fn register_slash_preference(aux, intentions_index: u32, p: SlashPreference) -> Result = 5;
fn note_missed_proposal(aux, offline_val_indices: Vec<u32>) -> Result = 6;
}
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -127,7 +153,9 @@ decl_module! {
fn set_sessions_per_era(new: T::BlockNumber) -> Result = 0;
fn set_bonding_duration(new: T::BlockNumber) -> Result = 1;
fn set_validator_count(new: u32) -> Result = 2;
fn force_new_era(should_slash: bool) -> Result = 3;
fn force_new_era(apply_rewards: bool) -> Result = 3;
fn set_offline_slash_grace(new: u32) -> Result = 4;
fn set_balance(who: RawAddress<T::AccountId, T::AccountIndex>, free: T::Balance, reserved: T::Balance) -> Result = 5;
}
}
@@ -136,8 +164,10 @@ decl_storage! {
// The length of the bonding duration in eras.
pub BondingDuration get(bonding_duration): b"sta:loc" => required T::BlockNumber;
// The length of a staking era in sessions.
// The ideal number of staking participants.
pub ValidatorCount get(validator_count): b"sta:vac" => required u32;
// Minimum number of staking participants before emergency conditions are imposed.
pub MinimumValidatorCount: b"sta:minimum_validator_count" => u32;
// The length of a staking era in sessions.
pub SessionsPerEra get(sessions_per_era): b"sta:spe" => required T::BlockNumber;
// The total amount of stake on the system.
@@ -159,9 +189,13 @@ decl_storage! {
pub SessionReward get(session_reward): b"sta:session_reward" => required T::Balance;
// Slash, per validator that is taken per abnormal era end.
pub EarlyEraSlash get(early_era_slash): b"sta:early_era_slash" => required T::Balance;
// Number of instances of offline reports before slashing begins for validators.
pub OfflineSlashGrace get(offline_slash_grace): b"sta:offline_slash_grace" => default u32;
// The current era index.
pub CurrentEra get(current_era): b"sta:era" => required T::BlockNumber;
// Preference over how many times the validator should get slashed for being offline before they are automatically unstaked.
pub SlashPreferenceOf get(slash_preference_of): b"sta:slash_preference_of" => default map [ T::AccountId => SlashPreference ];
// All the accounts with a desire to stake.
pub Intentions get(intentions): b"sta:wil:" => default Vec<T::AccountId>;
// All nominator -> nominee relationships.
@@ -177,8 +211,8 @@ decl_storage! {
// The current era stake threshold
pub StakeThreshold get(stake_threshold): b"sta:stake_threshold" => required T::Balance;
// The current bad validator slash.
pub CurrentSlash get(current_slash): b"sta:current_slash" => default T::Balance;
// The number of times a given validator has been reported offline. This gets decremented by one each era that passes.
pub SlashCount get(slash_count): b"sta:slash_count" => default map [ T::AccountId => u32 ];
// The next free enumeration set.
pub NextEnumSet get(next_enum_set): b"sta:next_enum" => required T::AccountIndex;
@@ -237,6 +271,10 @@ impl<T: Trait> Module<T> {
// PUBLIC IMMUTABLES
pub fn minimum_validator_count() -> usize {
<MinimumValidatorCount<T>>::get().map(|v| v as usize).unwrap_or(DEFAULT_MINIMUM_VALIDATOR_COUNT)
}
/// The length of a staking era in blocks.
pub fn era_length() -> T::BlockNumber {
Self::sessions_per_era() * <session::Module<T>>::length()
@@ -247,6 +285,20 @@ impl<T: Trait> Module<T> {
Self::free_balance(who) + Self::reserved_balance(who)
}
/// Balance of a (potential) validator that includes all nominators.
pub fn nomination_balance(who: &T::AccountId) -> T::Balance {
Self::nominators_for(who).iter()
.map(Self::voting_balance)
.fold(Zero::zero(), |acc, x| acc + x)
}
/// The total balance that can be slashed from an account.
pub fn slashable_balance(who: &T::AccountId) -> T::Balance {
Self::nominators_for(who).iter()
.map(Self::voting_balance)
.fold(Self::voting_balance(who), |acc, x| acc + x)
}
/// Some result as `slash(who, value)` (but without the side-effects) assuming there are no
/// balance changes in the meantime and only the reserved balance is not taken into account.
pub fn can_slash(who: &T::AccountId, value: T::Balance) -> bool {
@@ -263,6 +315,22 @@ impl<T: Trait> Module<T> {
}
}
/// Lookup an T::AccountIndex to get an Id, if there's one there.
pub fn lookup_index(index: T::AccountIndex) -> Option<T::AccountId> {
let enum_set_size = Self::enum_set_size();
let set = Self::enum_set(index / enum_set_size);
let i: usize = (index % enum_set_size).as_();
set.get(i).map(|x| x.clone())
}
/// `true` if the account `index` is ready for reclaim.
pub fn can_reclaim(try_index: T::AccountIndex) -> bool {
let enum_set_size = Self::enum_set_size();
let try_set = Self::enum_set(try_index / enum_set_size);
let i = (try_index % enum_set_size).as_();
i < try_set.len() && Self::voting_balance(&try_set[i]).is_zero()
}
/// The block at which the `who`'s funds become entirely liquid.
pub fn unlock_block(who: &T::AccountId) -> LockStatus<T::BlockNumber> {
match Self::bondage(who) {
@@ -336,18 +404,12 @@ impl<T: Trait> Module<T> {
/// Retract the desire to stake for the transactor.
///
/// Effects will be felt at the beginning of the next era.
fn unstake(aux: &T::PublicAux, position: u32) -> Result {
let aux = aux.ref_into();
let position = position as usize;
let mut intentions = <Intentions<T>>::get();
// let position = intentions.iter().position(|t| t == aux.ref_into()).ok_or("Cannot unstake if not already staked.")?;
if intentions.get(position) != Some(aux) {
return Err("Invalid index")
fn unstake(aux: &T::PublicAux, intentions_index: u32) -> Result {
// unstake fails in degenerate case of having too few existing staked parties
if Self::intentions().len() <= Self::minimum_validator_count() {
return Err("cannot unstake when there are too few staked participants")
}
intentions.swap_remove(position);
<Intentions<T>>::put(intentions);
<Bondage<T>>::insert(aux.ref_into(), Self::current_era() + Self::bonding_duration());
Ok(())
Self::apply_unstake(aux.ref_into(), intentions_index as usize)
}
fn nominate(aux: &T::PublicAux, target: RawAddress<T::AccountId, T::AccountIndex>) -> Result {
@@ -398,6 +460,64 @@ impl<T: Trait> Module<T> {
Ok(())
}
/// Set the given account's preference for slashing behaviour should they be a validator.
///
/// An error (no-op) if `Self::intentions()[intentions_index] != aux`.
fn register_slash_preference(
aux: &T::PublicAux,
intentions_index: u32,
p: SlashPreference
) -> Result {
let aux = aux.ref_into();
if Self::intentions().get(intentions_index as usize) != Some(aux) {
return Err("Invalid index")
}
<SlashPreferenceOf<T>>::insert(aux, p);
Ok(())
}
/// Note the previous block's validator missed their opportunity to propose a block. This only comes in
/// if 2/3+1 of the validators agree that no proposal was submitted. It's only relevant
/// for the previous block.
fn note_missed_proposal(aux: &T::PublicAux, offline_val_indices: Vec<u32>) -> Result {
assert!(aux.is_empty());
assert!(
<system::Module<T>>::extrinsic_index() == T::NOTE_MISSED_PROPOSAL_POSITION,
"note_missed_proposal extrinsic must be at position {} in the block",
T::NOTE_MISSED_PROPOSAL_POSITION
);
for validator_index in offline_val_indices.into_iter() {
let v = <session::Module<T>>::validators()[validator_index as usize].clone();
let slash_count = Self::slash_count(&v);
<SlashCount<T>>::insert(v.clone(), slash_count + 1);
let grace = Self::offline_slash_grace();
if slash_count >= grace {
let instances = slash_count - grace;
let slash = Self::early_era_slash() << instances;
let next_slash = slash << 1u32;
let _ = Self::slash_validator(&v, slash);
if instances >= Self::slash_preference_of(&v).unstake_threshold
|| Self::slashable_balance(&v) < next_slash
{
if let Some(pos) = Self::intentions().into_iter().position(|x| &x == &v) {
Self::apply_unstake(&v, pos)
.expect("pos derived correctly from Self::intentions(); \
apply_unstake can only fail if pos wrong; \
Self::intentions() doesn't change; qed");
}
let _ = Self::force_new_era(false);
}
}
}
Ok(())
}
// PRIV DISPATCH
/// Set the number of sessions in an era.
@@ -418,11 +538,25 @@ impl<T: Trait> Module<T> {
Ok(())
}
/// Force there to be a new era. This also forces a new session immediately after by
/// setting `normal_rotation` to be false. Validators will get slashed.
fn force_new_era(should_slash: bool) -> Result {
/// Force there to be a new era. This also forces a new session immediately after.
/// `apply_rewards` should be true for validators to get the session reward.
fn force_new_era(apply_rewards: bool) -> Result {
<ForcingNewEra<T>>::put(());
<session::Module<T>>::force_new_session(!should_slash)
<session::Module<T>>::force_new_session(apply_rewards)
}
/// Set the offline slash grace period.
fn set_offline_slash_grace(new: u32) -> Result {
<OfflineSlashGrace<T>>::put(&new);
Ok(())
}
/// Set the balances of a given account.
fn set_balance(who: Address<T>, free: T::Balance, reserved: T::Balance) -> Result {
let who = Self::lookup(who)?;
Self::set_free_balance(&who, free);
Self::set_reserved_balance(&who, reserved);
Ok(())
}
// PUBLIC MUTABLES (DANGEROUS)
@@ -606,60 +740,80 @@ impl<T: Trait> Module<T> {
}
}
/// Session has just changed. We need to determine whether we pay a reward, slash and/or
/// move to a new era.
fn new_session(actual_elapsed: T::Moment, bad_validators: Vec<T::AccountId>) {
let session_index = <session::Module<T>>::current_index();
let early_exit_era = !bad_validators.is_empty();
if early_exit_era {
// slash
let slash = Self::current_slash() + Self::early_era_slash();
<CurrentSlash<T>>::put(&slash);
for v in bad_validators.into_iter() {
if let Some(rem) = Self::slash(&v, slash) {
let noms = Self::current_nominators_for(&v);
let total = noms.iter().map(Self::voting_balance).fold(T::Balance::zero(), |acc, x| acc + x);
if !total.is_zero() {
let safe_mul_rational = |b| b * rem / total;// TODO: avoid overflow
for n in noms.iter() {
let _ = Self::slash(n, safe_mul_rational(Self::voting_balance(n))); // best effort - not much that can be done on fail.
}
}
}
}
} else {
// Zero any cumulative slash since we're healthy now.
<CurrentSlash<T>>::kill();
// reward
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_();
let reward = Self::session_reward() * T::Balance::sa(per65536) / T::Balance::sa(65536u64);
// apply good session reward
for v in <session::Module<T>>::validators().iter() {
let noms = Self::current_nominators_for(v);
let total = noms.iter().map(Self::voting_balance).fold(Self::voting_balance(v), |acc, x| acc + x);
if !total.is_zero() {
let safe_mul_rational = |b| b * reward / total;// TODO: avoid overflow
for n in noms.iter() {
let _ = Self::reward(n, safe_mul_rational(Self::voting_balance(n)));
}
let _ = Self::reward(v, safe_mul_rational(Self::voting_balance(v)));
}
}
/// Slash a given validator by a specific amount. Removes the slash from their balance by preference,
/// and reduces the nominators' balance if needed.
fn slash_validator(v: &T::AccountId, slash: T::Balance) {
// skip the slash in degenerate case of having only 4 staking participants despite having a larger
// desired number of validators (validator_count).
if Self::intentions().len() <= Self::minimum_validator_count() {
return
}
if <ForcingNewEra<T>>::take().is_some()
|| ((session_index - Self::last_era_length_change()) % Self::sessions_per_era()).is_zero()
|| early_exit_era
{
Self::new_era();
if let Some(rem) = Self::slash(v, slash) {
let noms = Self::current_nominators_for(v);
let total = noms.iter().map(Self::voting_balance).fold(T::Balance::zero(), |acc, x| acc + x);
if !total.is_zero() {
let safe_mul_rational = |b| b * rem / total;// TODO: avoid overflow
for n in noms.iter() {
let _ = Self::slash(n, safe_mul_rational(Self::voting_balance(n))); // best effort - not much that can be done on fail.
}
}
}
}
/// Balance of a (potential) validator that includes all nominators.
fn nomination_balance(who: &T::AccountId) -> T::Balance {
Self::nominators_for(who).iter().map(Self::voting_balance).fold(Zero::zero(), |acc, x| acc + x)
/// Reward a given validator by a specific amount. Add the reward to their, and their nominators'
/// balance, pro-rata.
fn reward_validator(who: &T::AccountId, reward: T::Balance) {
let noms = Self::current_nominators_for(who);
let total = noms.iter().map(Self::voting_balance).fold(Self::voting_balance(who), |acc, x| acc + x);
if !total.is_zero() {
let safe_mul_rational = |b| b * reward / total;// TODO: avoid overflow
for n in noms.iter() {
let _ = Self::reward(n, safe_mul_rational(Self::voting_balance(n)));
}
let _ = Self::reward(who, safe_mul_rational(Self::voting_balance(who)));
}
}
/// Actually carry out the unstake operation.
/// Assumes `intentions()[intentions_index] == who`.
fn apply_unstake(who: &T::AccountId, intentions_index: usize) -> Result {
let mut intentions = Self::intentions();
if intentions.get(intentions_index) != Some(who) {
return Err("Invalid index");
}
intentions.swap_remove(intentions_index);
<Intentions<T>>::put(intentions);
<SlashPreferenceOf<T>>::remove(who);
<SlashCount<T>>::remove(who);
<Bondage<T>>::insert(who, Self::current_era() + Self::bonding_duration());
Ok(())
}
/// Get the reward for the session, assuming it ends with this block.
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)
}
/// Session has just changed. We need to determine whether we pay a reward, slash and/or
/// move to a new era.
fn new_session(actual_elapsed: T::Moment, should_reward: bool) {
if should_reward {
// apply good session reward
let reward = Self::this_session_reward(actual_elapsed);
for v in <session::Module<T>>::validators().iter() {
Self::reward_validator(v, reward);
}
}
let session_index = <session::Module<T>>::current_index();
if <ForcingNewEra<T>>::take().is_some()
|| ((session_index - Self::last_era_length_change()) % Self::sessions_per_era()).is_zero()
{
Self::new_era();
}
}
/// The era has changed - enact new staking set.
@@ -678,18 +832,22 @@ impl<T: Trait> Module<T> {
}
}
let minimum_allowed = Self::early_era_slash();
// evaluate desired staking amounts and nominations and optimise to find the best
// combination of validators, then use session::internal::set_validators().
// for now, this just orders would-be stakers by their balances and chooses the top-most
// <ValidatorCount<T>>::get() of them.
// TODO: this is not sound. this should be moved to an off-chain solution mechanism.
let mut intentions = <Intentions<T>>::get()
let mut intentions = Self::intentions()
.into_iter()
.map(|v| (Self::voting_balance(&v) + Self::nomination_balance(&v), v))
.filter(|&(b, _)| b >= minimum_allowed)
.map(|v| (Self::slashable_balance(&v), v))
.collect::<Vec<_>>();
// Avoid reevaluate validator set if it would leave us with fewer than the minimum
// needed validators
if intentions.len() < Self::minimum_validator_count() {
return
}
intentions.sort_unstable_by(|&(ref b1, _), &(ref b2, _)| b2.cmp(&b1));
<StakeThreshold<T>>::put(
@@ -699,11 +857,15 @@ impl<T: Trait> Module<T> {
} else { Zero::zero() }
);
let vals = &intentions.into_iter()
.map(|(_, v)| v)
.take(<ValidatorCount<T>>::get() as usize)
.collect::<Vec<_>>();
.map(|(_, v)| v)
.take(<ValidatorCount<T>>::get() as usize)
.collect::<Vec<_>>();
for v in <session::Module<T>>::validators().iter() {
<CurrentNominatorsFor<T>>::remove(v);
let slash_count = <SlashCount<T>>::take(v);
if slash_count > 1 {
<SlashCount<T>>::insert(v, slash_count - 1);
}
}
for v in vals.iter() {
<CurrentNominatorsFor<T>>::insert(v, Self::nominators_for(v));
@@ -715,22 +877,6 @@ impl<T: Trait> Module<T> {
T::AccountIndex::sa(ENUM_SET_SIZE)
}
/// Lookup an T::AccountIndex to get an Id, if there's one there.
pub fn lookup_index(index: T::AccountIndex) -> Option<T::AccountId> {
let enum_set_size = Self::enum_set_size();
let set = Self::enum_set(index / enum_set_size);
let i: usize = (index % enum_set_size).as_();
set.get(i).map(|x| x.clone())
}
/// `true` if the account `index` is ready for reclaim.
pub fn can_reclaim(try_index: T::AccountIndex) -> bool {
let enum_set_size = Self::enum_set_size();
let try_set = Self::enum_set(try_index / enum_set_size);
let i = (try_index % enum_set_size).as_();
i < try_set.len() && Self::voting_balance(&try_set[i]).is_zero()
}
/// Register a new account (with existential balance).
fn new_account(who: &T::AccountId, balance: T::Balance) -> NewAccountOutcome {
let enum_set_size = Self::enum_set_size();
@@ -822,14 +968,14 @@ impl<T: Trait> Module<T> {
/// Increase TotalStake by Value.
pub fn increase_total_stake_by(value: T::Balance) {
if <TotalStake<T>>::exists() {
<TotalStake<T>>::put(<Module<T>>::total_stake() + value);
if let Some(v) = <Module<T>>::total_stake().checked_add(&value) {
<TotalStake<T>>::put(v);
}
}
/// Decrease TotalStake by Value.
pub fn decrease_total_stake_by(value: T::Balance) {
if <TotalStake<T>>::exists() {
<TotalStake<T>>::put(<Module<T>>::total_stake() - value);
if let Some(v) = <Module<T>>::total_stake().checked_sub(&value) {
<TotalStake<T>>::put(v);
}
}
}
@@ -839,9 +985,9 @@ impl<T: Trait> Executable for Module<T> {
}
}
impl<T: Trait> OnSessionChange<T::Moment, T::AccountId> for Module<T> {
fn on_session_change(elapsed: T::Moment, bad_validators: Vec<T::AccountId>) {
Self::new_session(elapsed, bad_validators);
impl<T: Trait> OnSessionChange<T::Moment> for Module<T> {
fn on_session_change(elapsed: T::Moment, should_reward: bool) {
Self::new_session(elapsed, should_reward);
}
}
@@ -45,7 +45,7 @@ impl system::Trait for Test {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 0;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = Staking;
}
@@ -87,8 +87,9 @@ pub fn new_test_ext(ext_deposit: u64, session_length: u64, sessions_per_era: u64
} else {
vec![(10, balance_factor), (20, balance_factor)]
},
intentions: vec![],
intentions: vec![10, 20],
validator_count: 2,
minimum_validator_count: 0,
bonding_duration: 3,
transaction_base_fee: 0,
transaction_byte_fee: 0,
@@ -98,6 +99,7 @@ pub fn new_test_ext(ext_deposit: u64, session_length: u64, sessions_per_era: u64
reclaim_rebate: 0,
session_reward: reward,
early_era_slash: if monied { 20 } else { 0 },
offline_slash_grace: 0,
}.build_storage().unwrap());
t.extend(timestamp::GenesisConfig::<Test>{
period: 5
+129 -13
View File
@@ -22,6 +22,133 @@ use super::*;
use runtime_io::with_externalities;
use mock::{Session, Staking, System, Timestamp, Test, new_test_ext};
#[test]
fn note_null_missed_proposal_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
assert_eq!(Staking::offline_slash_grace(), 0);
assert_eq!(Staking::slash_count(&10), 0);
assert_eq!(Staking::free_balance(&10), 1);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![]));
assert_eq!(Staking::slash_count(&10), 0);
assert_eq!(Staking::free_balance(&10), 1);
assert!(Staking::forcing_new_era().is_none());
});
}
#[test]
fn note_missed_proposal_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
Staking::set_free_balance(&10, 70);
assert_eq!(Staking::offline_slash_grace(), 0);
assert_eq!(Staking::slash_count(&10), 0);
assert_eq!(Staking::free_balance(&10), 70);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0]));
assert_eq!(Staking::slash_count(&10), 1);
assert_eq!(Staking::free_balance(&10), 50);
assert!(Staking::forcing_new_era().is_none());
});
}
#[test]
fn note_missed_proposal_exponent_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
Staking::set_free_balance(&10, 150);
assert_eq!(Staking::offline_slash_grace(), 0);
assert_eq!(Staking::slash_count(&10), 0);
assert_eq!(Staking::free_balance(&10), 150);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0]));
assert_eq!(Staking::slash_count(&10), 1);
assert_eq!(Staking::free_balance(&10), 130);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0]));
assert_eq!(Staking::slash_count(&10), 2);
assert_eq!(Staking::free_balance(&10), 90);
assert!(Staking::forcing_new_era().is_none());
});
}
#[test]
fn note_missed_proposal_grace_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
Staking::set_free_balance(&10, 70);
Staking::set_free_balance(&20, 70);
assert_ok!(Staking::set_offline_slash_grace(1));
assert_eq!(Staking::offline_slash_grace(), 1);
assert_eq!(Staking::slash_count(&10), 0);
assert_eq!(Staking::free_balance(&10), 70);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0]));
assert_eq!(Staking::slash_count(&10), 1);
assert_eq!(Staking::free_balance(&10), 70);
assert_eq!(Staking::slash_count(&20), 0);
assert_eq!(Staking::free_balance(&20), 70);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0, 1]));
assert_eq!(Staking::slash_count(&10), 2);
assert_eq!(Staking::free_balance(&10), 50);
assert_eq!(Staking::slash_count(&20), 1);
assert_eq!(Staking::free_balance(&20), 70);
assert!(Staking::forcing_new_era().is_none());
});
}
#[test]
fn note_missed_proposal_force_unstake_session_change_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
Staking::set_free_balance(&10, 70);
Staking::set_free_balance(&20, 70);
assert_ok!(Staking::stake(&1));
assert_eq!(Staking::slash_count(&10), 0);
assert_eq!(Staking::free_balance(&10), 70);
assert_eq!(Staking::intentions(), vec![10, 20, 1]);
assert_eq!(Session::validators(), vec![10, 20]);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0]));
assert_eq!(Staking::free_balance(&10), 50);
assert_eq!(Staking::slash_count(&10), 1);
assert_eq!(Staking::intentions(), vec![10, 20, 1]);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0]));
assert_eq!(Staking::intentions(), vec![1, 20]);
assert_eq!(Staking::free_balance(&10), 10);
assert!(Staking::forcing_new_era().is_some());
});
}
#[test]
fn note_missed_proposal_auto_unstake_session_change_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
Staking::set_free_balance(&10, 7000);
Staking::set_free_balance(&20, 7000);
assert_ok!(Staking::register_slash_preference(&10, 0, SlashPreference { unstake_threshold: 1 }));
assert_eq!(Staking::intentions(), vec![10, 20]);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0, 1]));
assert_eq!(Staking::free_balance(&10), 6980);
assert_eq!(Staking::free_balance(&20), 6980);
assert_eq!(Staking::intentions(), vec![10, 20]);
assert!(Staking::forcing_new_era().is_none());
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0, 1]));
assert_eq!(Staking::free_balance(&10), 6940);
assert_eq!(Staking::free_balance(&20), 6940);
assert_eq!(Staking::intentions(), vec![20]);
assert!(Staking::forcing_new_era().is_some());
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![1]));
assert_eq!(Staking::free_balance(&10), 6940);
assert_eq!(Staking::free_balance(&20), 6860);
assert_eq!(Staking::intentions(), vec![20]);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![1]));
assert_eq!(Staking::free_balance(&10), 6940);
assert_eq!(Staking::free_balance(&20), 6700);
assert_eq!(Staking::intentions(), vec![0u64; 0]);
});
}
#[test]
fn reward_should_work() {
with_externalities(&mut new_test_ext(0, 3, 3, 0, true, 10), || {
@@ -74,25 +201,19 @@ fn slashing_should_work() {
assert_eq!(Staking::voting_balance(&10), 1);
System::set_block_number(3);
Timestamp::set_timestamp(15); // on time.
Session::check_rotate_session();
assert_eq!(Staking::current_era(), 0);
assert_eq!(Session::current_index(), 1);
assert_eq!(Staking::voting_balance(&10), 11);
System::set_block_number(6);
Timestamp::set_timestamp(30); // on time.
Session::check_rotate_session();
assert_eq!(Staking::current_era(), 0);
assert_eq!(Session::current_index(), 2);
assert_eq!(Staking::voting_balance(&10), 21);
System::set_block_number(7);
::system::ExtrinsicIndex::<Test>::put(1);
Session::note_offline(&0, vec![0]).unwrap(); // val 10 reported bad.
Session::check_rotate_session();
assert_eq!(Staking::current_era(), 1);
assert_eq!(Session::current_index(), 3);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0, 1]));
assert_eq!(Staking::voting_balance(&10), 1);
});
}
@@ -312,12 +433,7 @@ fn nominating_slashes_should_work() {
assert_eq!(Staking::voting_balance(&4), 40);
System::set_block_number(5);
::system::ExtrinsicIndex::<Test>::put(1);
Session::note_offline(&0, vec![0, 1]).unwrap(); // both get reported offline.
assert_eq!(Session::blocks_remaining(), 1);
Session::check_rotate_session();
assert_eq!(Staking::current_era(), 2);
assert_ok!(Staking::note_missed_proposal(&Default::default(), vec![0, 1]));
assert_eq!(Staking::voting_balance(&1), 0);
assert_eq!(Staking::voting_balance(&2), 20);
assert_eq!(Staking::voting_balance(&3), 10);