diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ace14de91d142..4cba2fe46ad29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index c49662d9772d4..859ee0a5f2553 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2833,6 +2833,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", diff --git a/README.adoc b/README.adoc index 7c8be50e964eb..3e11d84eb6fa6 100644 --- a/README.adoc +++ b/README.adoc @@ -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 diff --git a/demo/cli/src/lib.rs b/demo/cli/src/lib.rs index a8d2fea6c188a..70d97204f0a9f 100644 --- a/demo/cli/src/lib.rs +++ b/demo/cli/src/lib.rs @@ -180,10 +180,12 @@ pub fn run(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 diff --git a/demo/executor/src/lib.rs b/demo/executor/src/lib.rs index 5911b9b48d87d..a75885bd911f2 100644 --- a/demo/executor/src/lib.rs +++ b/demo/executor/src/lib.rs @@ -102,6 +102,7 @@ mod tests { fn panic_execution_with_foreign_code_gives_error() { let mut t: TestExternalities = map![ twox_128(&>::key_for(alice())).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0], + twox_128(>::key()).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0], twox_128(>::key()).to_vec() => vec![70u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::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 = map![ twox_128(&>::key_for(alice())).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0], + twox_128(>::key()).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0], twox_128(>::key()).to_vec() => vec![70u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::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 = map![ twox_128(&>::key_for(alice())).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0], + twox_128(>::key()).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], @@ -163,6 +166,7 @@ mod tests { fn successful_execution_with_foreign_code_gives_ok() { let mut t: TestExternalities = map![ twox_128(&>::key_for(alice())).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0], + twox_128(>::key()).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::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 = map![ twox_128(&>::key_for(alice())).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0], + twox_128(>::key()).to_vec() => vec![69u8, 0, 0, 0, 0, 0, 0, 0], twox_128(>::key()).to_vec() => vec![70u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], @@ -384,6 +391,7 @@ mod tests { fn successful_execution_gives_ok() { let mut t: TestExternalities = map![ twox_128(&>::key_for(alice())).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0], + twox_128(>::key()).to_vec() => vec![111u8, 0, 0, 0, 0, 0, 0, 0], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], twox_128(>::key()).to_vec() => vec![0u8; 8], diff --git a/demo/runtime/src/lib.rs b/demo/runtime/src/lib.rs index ebc7c777da0c6..6ee577c8170eb 100644 --- a/demo/runtime/src/lib.rs +++ b/demo/runtime/src/lib.rs @@ -121,7 +121,7 @@ impl Convert 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; } diff --git a/substrate/runtime/contract/src/tests.rs b/substrate/runtime/contract/src/tests.rs index 53a324907e572..c248df4537f77 100644 --- a/substrate/runtime/contract/src/tests.rs +++ b/substrate/runtime/contract/src/tests.rs @@ -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(), ); diff --git a/substrate/runtime/council/src/lib.rs b/substrate/runtime/council/src/lib.rs index 78be6c1804168..0214d97dab570 100644 --- a/substrate/runtime/council/src/lib.rs +++ b/substrate/runtime/council/src/lib.rs @@ -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; } @@ -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::{ launch_period: 1, diff --git a/substrate/runtime/democracy/src/lib.rs b/substrate/runtime/democracy/src/lib.rs index 2c07d5b0d0f99..20ad062247495 100644 --- a/substrate/runtime/democracy/src/lib.rs +++ b/substrate/runtime/democracy/src/lib.rs @@ -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; } @@ -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::{ 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); }); diff --git a/substrate/runtime/executive/src/lib.rs b/substrate/runtime/executive/src/lib.rs index 6910f65ee3f3e..57b9a993c4df0 100644 --- a/substrate/runtime/executive/src/lib.rs +++ b/substrate/runtime/executive/src/lib.rs @@ -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; } @@ -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![], }, }, diff --git a/substrate/runtime/primitives/src/traits.rs b/substrate/runtime/primitives/src/traits.rs index 3c89087d70dc3..7b9fe8568e3d4 100644 --- a/substrate/runtime/primitives/src/traits.rs +++ b/substrate/runtime/primitives/src/traits.rs @@ -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 { @@ -132,6 +133,7 @@ pub trait SimpleArithmetic: Mul + MulAssign + Div + DivAssign + Rem + RemAssign + + Shl + Shr + CheckedAdd + CheckedSub + CheckedMul + @@ -145,6 +147,7 @@ impl + MulAssign + Div + DivAssign + Rem + RemAssign + + Shl + Shr + CheckedAdd + CheckedSub + CheckedMul + diff --git a/substrate/runtime/session/Cargo.toml b/substrate/runtime/session/Cargo.toml index 13244273c623f..481653125b941 100644 --- a/substrate/runtime/session/Cargo.toml +++ b/substrate/runtime/session/Cargo.toml @@ -34,4 +34,5 @@ std = [ "substrate-runtime-primitives/std", "substrate-runtime-consensus/std", "substrate-runtime-system/std", + "substrate-runtime-timestamp/std" ] diff --git a/substrate/runtime/session/src/lib.rs b/substrate/runtime/session/src/lib.rs index 59ba338e77a66..5583843fd960d 100644 --- a/substrate/runtime/session/src/lib.rs +++ b/substrate/runtime/session/src/lib.rs @@ -54,21 +54,21 @@ use runtime_support::dispatch::Result; use std::collections::HashMap; /// A session has changed. -pub trait OnSessionChange { +pub trait OnSessionChange { /// Session has changed. - fn on_session_change(time_elapsed: T, bad_validators: Vec); + fn on_session_change(time_elapsed: T, should_reward: bool); } -impl OnSessionChange for () { - fn on_session_change(_: T, _: Vec) {} +impl OnSessionChange 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; - type OnSessionChange: OnSessionChange; + type OnSessionChange: OnSessionChange; } 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 Module { } /// Forces a new session. - pub fn force_new_session(normal_rotation: bool) -> Result { - >::put(normal_rotation); + pub fn force_new_session(apply_rewards: bool) -> Result { + >::put(apply_rewards); Ok(()) } @@ -151,9 +151,9 @@ impl Module { pub fn note_offline(aux: &T::PublicAux, offline_val_indices: Vec) -> Result { assert!(aux.is_empty()); assert!( - >::extrinsic_index() == T::NOTE_OFFLINE_POSITION, - "note_offline extrinsic must be at position {} in the block", - T::NOTE_OFFLINE_POSITION + >::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 Module { // check block number and call next_session if necessary. let block_number = >::block_number(); let is_final_block = ((block_number - Self::last_length_change()) % Self::length()).is_zero(); - let bad_validators = >::take().unwrap_or_default(); - let should_end_session = >::take().is_some() || !bad_validators.is_empty() || is_final_block; + let (should_end_session, apply_rewards) = >::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) { + pub fn rotate_session(is_final_block: bool, apply_rewards: bool) { let now = >::get(); let time_elapsed = now.clone() - Self::current_start(); @@ -209,7 +209,7 @@ impl Module { >::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::::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(), || { diff --git a/substrate/runtime/staking/Cargo.toml b/substrate/runtime/staking/Cargo.toml index 4ca513a1f0c9d..7cffe09d01217 100644 --- a/substrate/runtime/staking/Cargo.toml +++ b/substrate/runtime/staking/Cargo.toml @@ -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" ] diff --git a/substrate/runtime/staking/src/genesis_config.rs b/substrate/runtime/staking/src/genesis_config.rs index af221cc3862ac..8da72bf1d92ab 100644 --- a/substrate/runtime/staking/src/genesis_config.rs +++ b/substrate/runtime/staking/src/genesis_config.rs @@ -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 { pub current_era: T::BlockNumber, pub balances: Vec<(T::AccountId, T::Balance)>, pub intentions: Vec, - 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 { pub existential_deposit: T::Balance, pub session_reward: T::Balance, pub early_era_slash: T::Balance, + pub offline_slash_grace: u32, } impl GenesisConfig where T::AccountId: From { @@ -58,6 +61,7 @@ impl GenesisConfig where T::AccountId: From { 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 GenesisConfig where T::AccountId: From { 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 GenesisConfig where T::AccountId: From { ], 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 GenesisConfig where T::AccountId: From { 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 Default for GenesisConfig { 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 Default for GenesisConfig { 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 primitives::BuildStorage for GenesisConfig { Self::hash(>::key()).to_vec() => self.intentions.encode(), Self::hash(>::key()).to_vec() => self.sessions_per_era.encode(), Self::hash(>::key()).to_vec() => self.validator_count.encode(), + Self::hash(>::key()).to_vec() => self.minimum_validator_count.encode(), Self::hash(>::key()).to_vec() => self.bonding_duration.encode(), Self::hash(>::key()).to_vec() => self.transaction_base_fee.encode(), Self::hash(>::key()).to_vec() => self.transaction_byte_fee.encode(), @@ -138,6 +148,7 @@ impl primitives::BuildStorage for GenesisConfig { Self::hash(>::key()).to_vec() => self.current_era.encode(), Self::hash(>::key()).to_vec() => self.session_reward.encode(), Self::hash(>::key()).to_vec() => self.early_era_slash.encode(), + Self::hash(>::key()).to_vec() => self.offline_slash_grace.encode(), Self::hash(>::key()).to_vec() => total_stake.encode() ]; diff --git a/substrate/runtime/staking/src/lib.rs b/substrate/runtime/staking/src/lib.rs index b62b585dbbaa2..dd1e6fcc2d5ca 100644 --- a/substrate/runtime/staking/src/lib.rs +++ b/substrate/runtime/staking/src/lib.rs @@ -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 OnAccountKill 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 + As + As; /// 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, 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) -> 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) -> 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, 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; // 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 Module { // PUBLIC IMMUTABLES + pub fn minimum_validator_count() -> usize { + >::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() * >::length() @@ -247,6 +285,20 @@ impl Module { 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 Module { } } + /// Lookup an T::AccountIndex to get an Id, if there's one there. + pub fn lookup_index(index: T::AccountIndex) -> Option { + 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 { match Self::bondage(who) { @@ -336,18 +404,12 @@ impl Module { /// 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 = >::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); - >::put(intentions); - >::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) -> Result { @@ -398,6 +460,64 @@ impl Module { 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") + } + + >::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) -> Result { + assert!(aux.is_empty()); + assert!( + >::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 = >::validators()[validator_index as usize].clone(); + let slash_count = Self::slash_count(&v); + >::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 Module { 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 { >::put(()); - >::force_new_session(!should_slash) + >::force_new_session(apply_rewards) + } + + /// Set the offline slash grace period. + fn set_offline_slash_grace(new: u32) -> Result { + >::put(&new); + Ok(()) + } + + /// Set the balances of a given account. + fn set_balance(who: Address, 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,62 +740,82 @@ impl Module { } } - /// 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) { - let session_index = >::current_index(); - let early_exit_era = !bad_validators.is_empty(); - - if early_exit_era { - // slash - let slash = Self::current_slash() + Self::early_era_slash(); - >::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. - } - } + /// 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 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. - >::kill(); + } + } - // reward - let ideal_elapsed = >::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); + /// 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); + >::put(intentions); + >::remove(who); + >::remove(who); + >::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 = >::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 >::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))); - } + Self::reward_validator(v, reward); } } + + let session_index = >::current_index(); if >::take().is_some() || ((session_index - Self::last_era_length_change()) % Self::sessions_per_era()).is_zero() - || early_exit_era { Self::new_era(); } } - /// 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) - } - /// The era has changed - enact new staking set. /// /// NOTE: This always happens immediately before a session change to ensure that new validators @@ -678,18 +832,22 @@ impl Module { } } - 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 // >::get() of them. // TODO: this is not sound. this should be moved to an off-chain solution mechanism. - let mut intentions = >::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::>(); + + // 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)); >::put( @@ -699,11 +857,15 @@ impl Module { } else { Zero::zero() } ); let vals = &intentions.into_iter() - .map(|(_, v)| v) - .take(>::get() as usize) - .collect::>(); + .map(|(_, v)| v) + .take(>::get() as usize) + .collect::>(); for v in >::validators().iter() { >::remove(v); + let slash_count = >::take(v); + if slash_count > 1 { + >::insert(v, slash_count - 1); + } } for v in vals.iter() { >::insert(v, Self::nominators_for(v)); @@ -715,22 +877,6 @@ impl Module { 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 { - 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 Module { /// Increase TotalStake by Value. pub fn increase_total_stake_by(value: T::Balance) { - if >::exists() { - >::put(>::total_stake() + value); + if let Some(v) = >::total_stake().checked_add(&value) { + >::put(v); } } /// Decrease TotalStake by Value. pub fn decrease_total_stake_by(value: T::Balance) { - if >::exists() { - >::put(>::total_stake() - value); + if let Some(v) = >::total_stake().checked_sub(&value) { + >::put(v); } } } @@ -839,9 +985,9 @@ impl Executable for Module { } } -impl OnSessionChange for Module { - fn on_session_change(elapsed: T::Moment, bad_validators: Vec) { - Self::new_session(elapsed, bad_validators); +impl OnSessionChange for Module { + fn on_session_change(elapsed: T::Moment, should_reward: bool) { + Self::new_session(elapsed, should_reward); } } diff --git a/substrate/runtime/staking/src/mock.rs b/substrate/runtime/staking/src/mock.rs index 2f3024d78d0ff..2efeab4d30743 100644 --- a/substrate/runtime/staking/src/mock.rs +++ b/substrate/runtime/staking/src/mock.rs @@ -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::{ period: 5 diff --git a/substrate/runtime/staking/src/tests.rs b/substrate/runtime/staking/src/tests.rs index b8e11cd98ffde..122b9202dde6b 100644 --- a/substrate/runtime/staking/src/tests.rs +++ b/substrate/runtime/staking/src/tests.rs @@ -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::::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::::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);