mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-09 20:11:09 +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:
@@ -57,7 +57,7 @@ test:rust:stable: &test
|
||||
- export PATH="${CI_PROJECT_DIR}/cargo/bin/:$PATH"
|
||||
- ./scripts/build.sh
|
||||
- ./scripts/build-demos.sh
|
||||
- time cargo test --all
|
||||
- time cargo test --all --release
|
||||
tags:
|
||||
- rust-stable
|
||||
|
||||
|
||||
Generated
+1
@@ -2834,6 +2834,7 @@ dependencies = [
|
||||
"serde 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.70 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"substrate-codec 0.1.0",
|
||||
"substrate-codec-derive 0.1.0",
|
||||
"substrate-keyring 0.1.0",
|
||||
"substrate-primitives 0.1.0",
|
||||
"substrate-runtime-consensus 0.1.0",
|
||||
|
||||
+28
-2
@@ -1,4 +1,30 @@
|
||||
# Substrate
|
||||
|
||||
Framework for blockchain innovators.
|
||||
More to come here.
|
||||
Next-generation framework for blockchain innovation.
|
||||
|
||||
## Description
|
||||
|
||||
At its heart, Substrate is a combination of three technologies: WebAssembly, Libp2p and AfG Consensus. It is both a library for building new blockchains with and a "skeleton key" of a blockchain client, able to synchronise to any Substrate-based chain.
|
||||
|
||||
Substrate chains have two distinct features that make them "next-generation": a dynamic, self-defining state-transition function and a progressive consensus algorithm with fast block production and adaptive, definite finality. The STF, encoded in WebAssembly, is known as the "runtime". This defines the `execute_block` function, and can specify everything from the staking algorithm, transaction semantics, logging mechanisms and governance procedures. Because the runtime is entirely dynamic all of these can be switched out or upgraded at any time. A Substrate chain is very much a "living organism".
|
||||
|
||||
## Roadmap
|
||||
|
||||
### So far
|
||||
|
||||
- 0.1 "PoC-1": PBFT consensus, Wasm runtime engine, basic runtime modules.
|
||||
- 0.2 "PoC-2": Libp2p
|
||||
|
||||
### In progress
|
||||
|
||||
- AfG consensus
|
||||
- Improved PoS
|
||||
- Smart contract runtime module
|
||||
|
||||
### The future
|
||||
|
||||
- Splitting out runtime modules into separate repo
|
||||
- Introduce substrate executable (the skeleton-key runtime)
|
||||
- Introduce basic but extensible transaction queue and block-builder and place them in the executable.
|
||||
- DAO runtime module
|
||||
- Audit
|
||||
|
||||
@@ -180,10 +180,12 @@ pub fn run<I, T>(args: I) -> error::Result<()> where
|
||||
existential_deposit: 500,
|
||||
balances: vec![(god_key.clone().into(), 1u64 << 63)].into_iter().collect(),
|
||||
validator_count: 12,
|
||||
minimum_validator_count: 4,
|
||||
sessions_per_era: 24, // 24 hours per era.
|
||||
bonding_duration: 90, // 90 days per bond.
|
||||
early_era_slash: 10000,
|
||||
session_reward: 100,
|
||||
offline_slash_grace: 0,
|
||||
}),
|
||||
democracy: Some(DemocracyConfig {
|
||||
launch_period: 120 * 24 * 14, // 2 weeks per public referendum
|
||||
|
||||
@@ -102,6 +102,7 @@ mod tests {
|
||||
fn panic_execution_with_foreign_code_gives_error() {
|
||||
let mut t: TestExternalities<KeccakHasher> = map![
|
||||
twox_128(&<staking::FreeBalance<Concrete>>::key_for(alice())).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TotalStake<Concrete>>::key()).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TransactionBaseFee<Concrete>>::key()).to_vec() => vec![70u8; 8],
|
||||
twox_128(<staking::TransactionByteFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::ExistentialDeposit<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
@@ -121,6 +122,7 @@ mod tests {
|
||||
fn bad_extrinsic_with_native_equivalent_code_gives_error() {
|
||||
let mut t: TestExternalities<KeccakHasher> = map![
|
||||
twox_128(&<staking::FreeBalance<Concrete>>::key_for(alice())).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TotalStake<Concrete>>::key()).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TransactionBaseFee<Concrete>>::key()).to_vec() => vec![70u8; 8],
|
||||
twox_128(<staking::TransactionByteFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::ExistentialDeposit<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
@@ -140,6 +142,7 @@ mod tests {
|
||||
fn successful_execution_with_native_equivalent_code_gives_ok() {
|
||||
let mut t: TestExternalities<KeccakHasher> = map![
|
||||
twox_128(&<staking::FreeBalance<Concrete>>::key_for(alice())).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TotalStake<Concrete>>::key()).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TransactionBaseFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::TransactionByteFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::ExistentialDeposit<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
@@ -163,6 +166,7 @@ mod tests {
|
||||
fn successful_execution_with_foreign_code_gives_ok() {
|
||||
let mut t: TestExternalities<KeccakHasher> = map![
|
||||
twox_128(&<staking::FreeBalance<Concrete>>::key_for(alice())).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TotalStake<Concrete>>::key()).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TransactionBaseFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::TransactionByteFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::ExistentialDeposit<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
@@ -199,6 +203,7 @@ mod tests {
|
||||
balances: vec![(alice(), 111)],
|
||||
intentions: vec![alice(), bob(), Charlie.to_raw_public().into()],
|
||||
validator_count: 3,
|
||||
minimum_validator_count: 0,
|
||||
bonding_duration: 0,
|
||||
transaction_base_fee: 1,
|
||||
transaction_byte_fee: 0,
|
||||
@@ -208,6 +213,7 @@ mod tests {
|
||||
reclaim_rebate: 0,
|
||||
early_era_slash: 0,
|
||||
session_reward: 0,
|
||||
offline_slash_grace: 0,
|
||||
}),
|
||||
democracy: Some(Default::default()),
|
||||
council: Some(Default::default()),
|
||||
@@ -250,7 +256,7 @@ mod tests {
|
||||
// Blake
|
||||
// hex!("3437bf4b182ab17bb322af5c67e55f6be487a77084ad2b4e27ddac7242e4ad21").into(),
|
||||
// Keccak
|
||||
hex!("c563199c60df7d914262b1775b284870f3a5da2f24b56d2c6288b37c815a6cd9").into(),
|
||||
hex!("856f39cc430b2ecc2b94f55f0df44b28a25ab3ed341a60bdf0b8f382616f675f").into(),
|
||||
vec![BareExtrinsic {
|
||||
signed: alice(),
|
||||
index: 0,
|
||||
@@ -266,7 +272,7 @@ mod tests {
|
||||
// Blake
|
||||
// hex!("741fcb660e6fa9f625fbcd993b49f6c1cc4040f5e0cc8727afdedf11fd3c464b").into(),
|
||||
// Keccak
|
||||
hex!("83f71d5475f63350825b0301de322233d3711a9f3fcfd74050d1534af47a36b3").into(),
|
||||
hex!("32cb12103306811f4febf3a93c893ebd896f0df5bcf285912d406b43d9f041aa").into(),
|
||||
vec![
|
||||
BareExtrinsic {
|
||||
signed: bob(),
|
||||
@@ -289,7 +295,7 @@ mod tests {
|
||||
// Blake
|
||||
// hex!("2c7231a9c210a7aa4bea169d944bc4aaacd517862b244b8021236ffa7f697991").into(),
|
||||
// Keccak
|
||||
hex!("06d026c0d687ec583660a6052de6f89acdb24ea964d06be3831c837c3c426966").into(),
|
||||
hex!("f7bdc5a3409738285c04585ec436c5c9c3887448f7cf1b5086664517681eb7c1").into(),
|
||||
vec![BareExtrinsic {
|
||||
signed: alice(),
|
||||
index: 0,
|
||||
@@ -364,6 +370,7 @@ mod tests {
|
||||
fn panic_execution_gives_error() {
|
||||
let mut t: TestExternalities<KeccakHasher> = map![
|
||||
twox_128(&<staking::FreeBalance<Concrete>>::key_for(alice())).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TotalStake<Concrete>>::key()).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TransactionBaseFee<Concrete>>::key()).to_vec() => vec![70u8; 8],
|
||||
twox_128(<staking::TransactionByteFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::ExistentialDeposit<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
@@ -384,6 +391,7 @@ mod tests {
|
||||
fn successful_execution_gives_ok() {
|
||||
let mut t: TestExternalities<KeccakHasher> = map![
|
||||
twox_128(&<staking::FreeBalance<Concrete>>::key_for(alice())).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TotalStake<Concrete>>::key()).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0],
|
||||
twox_128(<staking::TransactionBaseFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::TransactionByteFee<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
twox_128(<staking::ExistentialDeposit<Concrete>>::key()).to_vec() => vec![0u8; 8],
|
||||
|
||||
@@ -121,7 +121,7 @@ impl Convert<AccountId, SessionKey> for SessionKeyConversion {
|
||||
}
|
||||
|
||||
impl session::Trait for Concrete {
|
||||
const NOTE_OFFLINE_POSITION: u32 = 1;
|
||||
const NOTE_MISSED_PROPOSAL_POSITION: u32 = 1;
|
||||
type ConvertAccountIdToSessionKey = SessionKeyConversion;
|
||||
type OnSessionChange = Staking;
|
||||
}
|
||||
|
||||
@@ -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