mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-06 09:07:30 +00:00
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:
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user