diff --git a/Cargo.lock b/Cargo.lock index c0c3ef6b80b89..d5fdf37d427e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2356,6 +2356,7 @@ dependencies = [ "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", "sr-std 2.0.0", "sr-version 2.0.0", "srml-authorship 0.1.0", @@ -2371,6 +2372,7 @@ dependencies = [ "srml-im-online 0.1.0", "srml-indices 2.0.0", "srml-membership 2.0.0", + "srml-offences 1.0.0", "srml-session 2.0.0", "srml-staking 2.0.0", "srml-sudo 2.0.0", @@ -3650,6 +3652,15 @@ dependencies = [ "wasmi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "sr-staking-primitives" +version = "2.0.0" +dependencies = [ + "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-primitives 2.0.0", + "sr-std 2.0.0", +] + [[package]] name = "sr-std" version = "2.0.0" @@ -3694,7 +3705,6 @@ dependencies = [ "sr-primitives 2.0.0", "sr-std 2.0.0", "srml-session 2.0.0", - "srml-staking 2.0.0", "srml-support 2.0.0", "srml-system 2.0.0", "srml-timestamp 2.0.0", @@ -3729,6 +3739,7 @@ dependencies = [ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", "sr-std 2.0.0", "srml-session 2.0.0", "srml-support 2.0.0", @@ -3897,6 +3908,7 @@ dependencies = [ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", "sr-std 2.0.0", "srml-finality-tracker 2.0.0", "srml-session 2.0.0", @@ -3914,6 +3926,7 @@ dependencies = [ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", "sr-std 2.0.0", "srml-session 2.0.0", "srml-support 2.0.0", @@ -3963,6 +3976,22 @@ dependencies = [ "substrate-primitives 2.0.0", ] +[[package]] +name = "srml-offences" +version = "1.0.0" +dependencies = [ + "parity-scale-codec 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-io 2.0.0", + "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", + "sr-std 2.0.0", + "srml-balances 2.0.0", + "srml-support 2.0.0", + "srml-system 2.0.0", + "substrate-primitives 2.0.0", +] + [[package]] name = "srml-session" version = "2.0.0" @@ -3973,6 +4002,7 @@ dependencies = [ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", "sr-std 2.0.0", "srml-support 2.0.0", "srml-system 2.0.0", @@ -3992,6 +4022,7 @@ dependencies = [ "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", "sr-primitives 2.0.0", + "sr-staking-primitives 2.0.0", "sr-std 2.0.0", "srml-authorship 0.1.0", "srml-balances 2.0.0", diff --git a/Cargo.toml b/Cargo.toml index be6c89c2b7b10..de0abb643d114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "core/sr-api-macros", "core/sr-io", "core/sr-primitives", + "core/sr-staking-primitives", "core/sr-sandbox", "core/sr-std", "core/sr-version", @@ -84,6 +85,7 @@ members = [ "srml/indices", "srml/membership", "srml/metadata", + "srml/offences", "srml/session", "srml/staking", "srml/sudo", diff --git a/core/cli/src/params.rs b/core/cli/src/params.rs index b17fc114c4f07..72adc552b9ecd 100644 --- a/core/cli/src/params.rs +++ b/core/cli/src/params.rs @@ -119,6 +119,7 @@ pub struct NetworkConfigurationParams { } arg_enum! { + #[allow(missing_docs)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum NodeKeyType { Secp256k1, diff --git a/core/sr-io/src/lib.rs b/core/sr-io/src/lib.rs index 91b27efba0304..b79c13fb12934 100644 --- a/core/sr-io/src/lib.rs +++ b/core/sr-io/src/lib.rs @@ -269,6 +269,7 @@ export_api! { /// Even if this function returns `true`, it does not mean that any keys are configured /// and that the validator is registered in the chain. fn is_validator() -> bool; + /// Submit transaction to the pool. /// /// The transaction will end up in the pool. diff --git a/core/sr-primitives/src/lib.rs b/core/sr-primitives/src/lib.rs index 636a97ce9200b..a6a66b80d4c1e 100644 --- a/core/sr-primitives/src/lib.rs +++ b/core/sr-primitives/src/lib.rs @@ -298,6 +298,18 @@ impl Perbill { Perbill(part as u32) } + + /// Return the product of multiplication of this value by itself. + pub fn square(self) -> Self { + let p: u64 = self.0 as u64 * self.0 as u64; + let q: u64 = 1_000_000_000 * 1_000_000_000; + Self::from_rational_approximation(p, q) + } + + /// Take out the raw parts-per-billions. + pub fn into_parts(self) -> u32 { + self.0 + } } impl ops::Mul for Perbill @@ -959,4 +971,23 @@ mod tests { ((Into::::into(std::u128::MAX) * 999_999u32) / 1_000_000u32).as_u128() ); } + + #[test] + fn per_bill_square() { + const FIXTURES: &[(u32, u32)] = &[ + (0, 0), + (1250000, 1562), // (0.00125, 0.000001562) + (255300000, 65178090), // (0.2553, 0.06517809) + (500000000, 250000000), // (0.5, 0.25) + (999995000, 999990000), // (0.999995, 0.999990000, but ideally 0.99999000002) + (1000000000, 1000000000), + ]; + + for &(x, r) in FIXTURES { + assert_eq!( + Perbill::from_parts(x).square(), + Perbill::from_parts(r), + ); + } + } } diff --git a/core/sr-staking-primitives/Cargo.toml b/core/sr-staking-primitives/Cargo.toml new file mode 100644 index 0000000000000..25e8f4ccf1529 --- /dev/null +++ b/core/sr-staking-primitives/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sr-staking-primitives" +version = "2.0.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } +sr-primitives = { path = "../sr-primitives", default-features = false } +rstd = { package = "sr-std", path = "../sr-std", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "sr-primitives/std", + "rstd/std", +] diff --git a/core/sr-staking-primitives/src/lib.rs b/core/sr-staking-primitives/src/lib.rs new file mode 100644 index 0000000000000..63a5eb29279d1 --- /dev/null +++ b/core/sr-staking-primitives/src/lib.rs @@ -0,0 +1,32 @@ + +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +#![cfg_attr(not(feature = "std"), no_std)] + +//! A crate which contains primitives that are useful for implementation that uses staking +//! approaches in general. Definitions related to sessions, slashing, etc go here. + +use rstd::vec::Vec; + +pub mod offence; + +/// Simple index type with which we can count sessions. +pub type SessionIndex = u32; + +/// A trait for getting the currently elected validator set without coupling to the module that +/// provides this information. +pub trait CurrentElectedSet { + /// Returns the validator ids for the currently elected validator set. + fn current_elected_set() -> Vec; +} diff --git a/core/sr-staking-primitives/src/offence.rs b/core/sr-staking-primitives/src/offence.rs new file mode 100644 index 0000000000000..c076103c18332 --- /dev/null +++ b/core/sr-staking-primitives/src/offence.rs @@ -0,0 +1,142 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Common traits and types that are useful for describing offences for usage in environments +//! that use staking. + +use rstd::vec::Vec; + +use codec::{Encode, Decode}; +use sr_primitives::Perbill; + +use crate::SessionIndex; + +/// The kind of an offence, is a byte string representing some kind identifier +/// e.g. `b"im-online:offlin"`, `b"babe:equivocatio"` +// TODO [slashing]: Is there something better we can have here that is more natural but still +// flexible? as you see in examples, they get cut off with long names. +pub type Kind = [u8; 16]; + +/// Number of times the offence of this authority was already reported in the past. +/// +/// Note that we don't buffer offence reporting, so every time we see a new offence +/// of the same kind, we will report past authorities again. +/// This counter keeps track of how many times the authority was already reported in the past, +/// so that we can slash it accordingly. +pub type OffenceCount = u32; + +/// A trait implemented by an offence report. +/// +/// This trait assumes that the offence is legitimate and was validated already. +/// +/// Examples of offences include: a BABE equivocation or a GRANDPA unjustified vote. +pub trait Offence { + /// Identifier which is unique for this kind of an offence. + const ID: Kind; + + /// A type that represents a point in time on an abstract timescale. + /// + /// See `Offence::time_slot` for details. The only requirement is that such timescale could be + /// represented by a single `u128` value. + type TimeSlot: Clone + codec::Codec + Ord; + + /// The list of all offenders involved in this incident. + /// + /// The list has no duplicates, so it is rather a set. + fn offenders(&self) -> Vec; + + /// The session index that is used for querying the validator set for the `slash_fraction` + /// function. + /// + /// This is used for filtering historical sessions. + fn session_index(&self) -> SessionIndex; + + /// Return a validator set count at the time when the offence took place. + fn validator_set_count(&self) -> u32; + + /// A point in time when this offence happened. + /// + /// This is used for looking up offences that happened at the "same time". + /// + /// The timescale is abstract and doesn't have to be the same across different implementations + /// of this trait. The value doesn't represent absolute timescale though since it is interpreted + /// along with the `session_index`. Two offences are considered to happen at the same time iff + /// both `session_index` and `time_slot` are equal. + /// + /// As an example, for GRANDPA timescale could be a round number and for BABE it could be a slot + /// number. Note that for GRANDPA the round number is reset each epoch. + fn time_slot(&self) -> Self::TimeSlot; + + /// A slash fraction of the total exposure that should be slashed for this + /// particular offence kind for the given parameters that happened at a singular `TimeSlot`. + /// + /// `offenders_count` - the count of unique offending authorities. It is >0. + /// `validator_set_count` - the cardinality of the validator set at the time of offence. + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill; +} + +/// A trait for decoupling offence reporters from the actual handling of offence reports. +pub trait ReportOffence> { + /// Report an `offence` and reward given `reporters`. + fn report_offence(reporters: Vec, offence: O); +} + +impl> ReportOffence for () { + fn report_offence(_reporters: Vec, _offence: O) {} +} + +/// A trait to take action on an offence. +/// +/// Used to decouple the module that handles offences and +/// the one that should punish for those offences. +pub trait OnOffenceHandler { + /// A handler for an offence of a particular kind. + /// + /// Note that this contains a list of all previous offenders + /// as well. The implementer should cater for a case, where + /// the same authorities were reported for the same offence + /// in the past (see `OffenceCount`). + /// + /// The vector of `slash_fraction` contains `Perbill`s + /// the authorities should be slashed and is computed + /// according to the `OffenceCount` already. This is of the same length as `offenders.` + /// Zero is a valid value for a fraction. + fn on_offence( + offenders: &[OffenceDetails], + slash_fraction: &[Perbill], + ); +} + +impl OnOffenceHandler for () { + fn on_offence( + _offenders: &[OffenceDetails], + _slash_fraction: &[Perbill], + ) {} +} + +/// A details about an offending authority for a particular kind of offence. +#[derive(Clone, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct OffenceDetails { + /// The offending authority id + pub offender: Offender, + /// A list of reporters of offences of this authority ID. Possibly empty where there are no + /// particular reporters. + pub reporters: Vec, +} diff --git a/node-template/runtime/src/lib.rs b/node-template/runtime/src/lib.rs index 16235db1183c3..27ae69bd56ed4 100644 --- a/node-template/runtime/src/lib.rs +++ b/node-template/runtime/src/lib.rs @@ -143,7 +143,6 @@ impl system::Trait for Runtime { } impl aura::Trait for Runtime { - type HandleReport = (); type AuthorityId = AuraId; } diff --git a/node/cli/src/chain_spec.rs b/node/cli/src/chain_spec.rs index 0784b02dc1b13..a25dc5cc2f218 100644 --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -20,7 +20,7 @@ use primitives::{Pair, Public, crypto::UncheckedInto}; pub use node_primitives::{AccountId, Balance}; use node_runtime::{ BabeConfig, BalancesConfig, ContractsConfig, CouncilConfig, DemocracyConfig, - ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, Perbill, + ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, SessionConfig, SessionKeys, StakerStatus, StakingConfig, SudoConfig, SystemConfig, TechnicalCommitteeConfig, WASM_BINARY, }; @@ -32,6 +32,7 @@ use substrate_telemetry::TelemetryEndpoints; use grandpa_primitives::{AuthorityId as GrandpaId}; use babe_primitives::{AuthorityId as BabeId}; use im_online::AuthorityId as ImOnlineId; +use sr_primitives::Perbill; const STAGING_TELEMETRY_URL: &str = "wss://telemetry.polkadot.io/submit/"; @@ -133,14 +134,13 @@ fn staging_testnet_config_genesis() -> GenesisConfig { }), staking: Some(StakingConfig { current_era: 0, - offline_slash: Perbill::from_parts(1_000_000), validator_count: 7, - offline_slash_grace: 4, minimum_validator_count: 4, stakers: initial_authorities.iter().map(|x| { (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator) }).collect(), invulnerables: initial_authorities.iter().map(|x| x.0.clone()).collect(), + slash_reward_fraction: Perbill::from_percent(10), .. Default::default() }), democracy: Some(DemocracyConfig::default()), @@ -262,12 +262,11 @@ pub fn testnet_genesis( current_era: 0, minimum_validator_count: 1, validator_count: 2, - offline_slash: Perbill::zero(), - offline_slash_grace: 0, stakers: initial_authorities.iter().map(|x| { (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator) }).collect(), invulnerables: initial_authorities.iter().map(|x| x.0.clone()).collect(), + slash_reward_fraction: Perbill::from_percent(10), .. Default::default() }), democracy: Some(DemocracyConfig::default()), @@ -300,7 +299,7 @@ pub fn testnet_genesis( babe: Some(BabeConfig { authorities: vec![], }), - im_online: Some(ImOnlineConfig{ + im_online: Some(ImOnlineConfig { keys: vec![], }), grandpa: Some(GrandpaConfig { diff --git a/node/executor/src/lib.rs b/node/executor/src/lib.rs index c2e442fd580b1..eece362ea8d9d 100644 --- a/node/executor/src/lib.rs +++ b/node/executor/src/lib.rs @@ -375,8 +375,7 @@ mod tests { ], validator_count: 3, minimum_validator_count: 0, - offline_slash: Perbill::zero(), - offline_slash_grace: 0, + slash_reward_fraction: Perbill::from_percent(10), invulnerables: vec![alice(), bob(), charlie()], .. Default::default() }), diff --git a/node/runtime/Cargo.toml b/node/runtime/Cargo.toml index 7ffb29e0784ce..188d0c6f97180 100644 --- a/node/runtime/Cargo.toml +++ b/node/runtime/Cargo.toml @@ -13,6 +13,7 @@ primitives = { package = "substrate-primitives", path = "../../core/primitives" client = { package = "substrate-client", path = "../../core/client", default-features = false } rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } offchain-primitives = { package = "substrate-offchain-primitives", path = "../../core/offchain/primitives", default-features = false } version = { package = "sr-version", path = "../../core/sr-version", default-features = false } support = { package = "srml-support", path = "../../srml/support", default-features = false } @@ -37,6 +38,7 @@ timestamp = { package = "srml-timestamp", path = "../../srml/timestamp", default treasury = { package = "srml-treasury", path = "../../srml/treasury", default-features = false } sudo = { package = "srml-sudo", path = "../../srml/sudo", default-features = false } im-online = { package = "srml-im-online", path = "../../srml/im-online", default-features = false } +offences = { package = "srml-offences", path = "../../srml/offences", default-features = false } node-primitives = { path = "../primitives", default-features = false } rustc-hex = { version = "2.0", optional = true } serde = { version = "1.0", optional = true } @@ -52,39 +54,41 @@ no_std = [ "contracts/core", ] std = [ - "codec/std", - "primitives/std", - "rstd/std", - "sr-primitives/std", - "support/std", "authorship/std", - "babe/std", "babe-primitives/std", - "consensus-primitives/std", + "babe/std", "balances/std", - "contracts/std", + "client/std", + "codec/std", "collective/std", + "consensus-primitives/std", + "contracts/std", "democracy/std", "elections/std", "executive/std", "finality-tracker/std", "grandpa/std", + "im-online/std", "indices/std", "membership/std", + "node-primitives/std", + "offchain-primitives/std", + "offences/std", + "primitives/std", + "rstd/std", + "rustc-hex", + "safe-mix/std", + "serde", "session/std", + "sr-primitives/std", + "sr-staking-primitives/std", "staking/std", + "substrate-keyring", + "substrate-session/std", + "sudo/std", + "support/std", "system/std", "timestamp/std", "treasury/std", - "sudo/std", "version/std", - "node-primitives/std", - "serde", - "safe-mix/std", - "client/std", - "rustc-hex", - "substrate-keyring", - "offchain-primitives/std", - "im-online/std", - "substrate-session/std", ] diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index af7533863e083..6c965e7a8f8e4 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -80,8 +80,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 145, - impl_version: 145, + spec_version: 146, + impl_version: 146, apis: RUNTIME_API_VERSIONS, }; @@ -225,7 +225,7 @@ impl session::historical::Trait for Runtime { } parameter_types! { - pub const SessionsPerEra: session::SessionIndex = 6; + pub const SessionsPerEra: sr_staking_primitives::SessionIndex = 6; pub const BondingDuration: staking::EraIndex = 24 * 28; } @@ -395,6 +395,14 @@ impl im_online::Trait for Runtime { type Call = Call; type Event = Event; type UncheckedExtrinsic = UncheckedExtrinsic; + type ReportUnresponsiveness = Offences; + type CurrentElectedSet = staking::CurrentElectedStashAccounts; +} + +impl offences::Trait for Runtime { + type Event = Event; + type IdentificationTuple = session::historical::IdentificationTuple; + type OnOffenceHandler = Staking; } impl grandpa::Trait for Runtime { @@ -437,6 +445,7 @@ construct_runtime!( Contracts: contracts, Sudo: sudo, ImOnline: im_online::{Module, Call, Storage, Event, ValidateUnsigned, Config}, + Offences: offences::{Module, Call, Storage, Event}, } ); diff --git a/srml/aura/Cargo.toml b/srml/aura/Cargo.toml index 98612cb6750ff..06a5a94635a59 100644 --- a/srml/aura/Cargo.toml +++ b/srml/aura/Cargo.toml @@ -5,39 +5,37 @@ authors = ["Parity Technologies "] edition = "2018" [dependencies] +app-crypto = { package = "substrate-application-crypto", path = "../../core/application-crypto", default-features = false } codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } -serde = { version = "1.0", optional = true } inherents = { package = "substrate-inherents", path = "../../core/inherents", default-features = false } +primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false } rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } +serde = { version = "1.0", optional = true } +session = { package = "srml-session", path = "../session", default-features = false } sr-primitives = { path = "../../core/sr-primitives", default-features = false } -primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false } -app-crypto = { package = "substrate-application-crypto", path = "../../core/application-crypto", default-features = false } runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false, features = [ "wasm-nice-panic-message" ] } srml-support = { path = "../support", default-features = false } +substrate-consensus-aura-primitives = { path = "../../core/consensus/aura/primitives", default-features = false} system = { package = "srml-system", path = "../system", default-features = false } timestamp = { package = "srml-timestamp", path = "../timestamp", default-features = false } -staking = { package = "srml-staking", path = "../staking", default-features = false } -session = { package = "srml-session", path = "../session", default-features = false } -substrate-consensus-aura-primitives = { path = "../../core/consensus/aura/primitives", default-features = false} [dev-dependencies] lazy_static = "1.0" parking_lot = "0.9.0" -runtime_io = { package = "sr-io", path = "../../core/sr-io" } [features] default = ["std"] std = [ - "serde", + "app-crypto/std", "codec/std", + "inherents/std", + "runtime_io/std", + "primitives/std", "rstd/std", - "srml-support/std", + "serde", "sr-primitives/std", - "primitives/std", + "srml-support/std", + "substrate-consensus-aura-primitives/std", "system/std", "timestamp/std", - "staking/std", - "inherents/std", - "substrate-consensus-aura-primitives/std", - "app-crypto/std", ] diff --git a/srml/aura/src/lib.rs b/srml/aura/src/lib.rs index 2fd317766a260..fb131e445f49f 100644 --- a/srml/aura/src/lib.rs +++ b/srml/aura/src/lib.rs @@ -31,9 +31,6 @@ //! //! ## Related Modules //! -//! - [Staking](../srml_staking/index.html): The Staking module is called in Aura to enforce slashing -//! if validators miss a certain number of slots (see the [`StakingSlasher`](./struct.StakingSlasher.html) -//! struct and associated method). //! - [Timestamp](../srml_timestamp/index.html): The Timestamp module is used in Aura to track //! consensus rounds (via `slots`). //! - [Consensus](../srml_consensus/index.html): The Consensus module does not relate directly to Aura, @@ -55,7 +52,7 @@ use codec::Encode; use srml_support::{decl_storage, decl_module, Parameter, storage::StorageValue, traits::Get}; use app_crypto::AppPublic; use sr_primitives::{ - traits::{SaturatedConversion, Saturating, Zero, One, Member, IsMember}, generic::DigestItem, + traits::{SaturatedConversion, Saturating, Zero, Member, IsMember}, generic::DigestItem, }; use timestamp::OnTimestampSet; #[cfg(feature = "std")] @@ -142,19 +139,7 @@ impl ProvideInherentData for InherentDataProvider { } } -/// Something that can handle Aura consensus reports. -pub trait HandleReport { - fn handle_report(report: AuraReport); -} - -impl HandleReport for () { - fn handle_report(_report: AuraReport) { } -} - pub trait Trait: timestamp::Trait { - /// The logic for handling reports. - type HandleReport: HandleReport; - /// The identifier type for an authority. type AuthorityId: Member + Parameter + AppPublic + Default; } @@ -245,34 +230,6 @@ impl IsMember for Module { } } -/// A report of skipped authorities in Aura. -#[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct AuraReport { - // The first skipped slot. - start_slot: usize, - // The number of times authorities were skipped. - skipped: usize, -} - -impl AuraReport { - /// Call the closure with (`validator_indices`, `punishment_count`) for each - /// validator to punish. - pub fn punish(&self, validator_count: usize, mut punish_with: F) - where F: FnMut(usize, usize) - { - // If all validators have been skipped, then it implies some sort of - // systematic problem common to all rather than a minority of validators - // not fulfilling their specific duties. In this case, it doesn't make - // sense to punish anyone, so we guard against it. - if self.skipped < validator_count { - for index in 0..self.skipped { - punish_with((self.start_slot + index) % validator_count, 1); - } - } - } -} - impl Module { /// Determine the Aura slot-duration based on the Timestamp module configuration. pub fn slot_duration() -> T::Moment { @@ -281,7 +238,7 @@ impl Module { ::MinimumPeriod::get().saturating_mul(2.into()) } - fn on_timestamp_set(now: T::Moment, slot_duration: T::Moment) { + fn on_timestamp_set(now: T::Moment, slot_duration: T::Moment) { let last = Self::last(); ::LastTimestamp::put(now.clone()); @@ -292,42 +249,17 @@ impl Module { assert!(!slot_duration.is_zero(), "Aura slot duration cannot be zero."); let last_slot = last / slot_duration.clone(); - let first_skipped = last_slot.clone() + One::one(); let cur_slot = now / slot_duration; assert!(last_slot < cur_slot, "Only one block may be authored per slot."); - if cur_slot == first_skipped { return } - - let skipped_slots = cur_slot - last_slot - One::one(); - H::handle_report(AuraReport { - start_slot: first_skipped.saturated_into::(), - skipped: skipped_slots.saturated_into::(), - }) + // TODO [#3398] Generate offence report for all authorities that skipped their slots. } } impl OnTimestampSet for Module { fn on_timestamp_set(moment: T::Moment) { - Self::on_timestamp_set::(moment, Self::slot_duration()) - } -} - -/// A type for performing slashing based on Aura reports. -pub struct StakingSlasher(::rstd::marker::PhantomData); - -impl HandleReport for StakingSlasher { - fn handle_report(report: AuraReport) { - use staking::SessionInterface; - let validators = T::SessionInterface::validators(); - - report.punish( - validators.len(), - |idx, slash_count| { - let v = validators[idx].clone(); - staking::Module::::on_offline_validator(v, slash_count); - } - ); + Self::on_timestamp_set(moment, Self::slot_duration()) } } diff --git a/srml/aura/src/mock.rs b/srml/aura/src/mock.rs index aac3f63f974ae..6fef48d3d7137 100644 --- a/srml/aura/src/mock.rs +++ b/srml/aura/src/mock.rs @@ -69,7 +69,6 @@ impl timestamp::Trait for Test { } impl Trait for Test { - type HandleReport = (); type AuthorityId = AuthorityId; } @@ -81,5 +80,4 @@ pub fn new_test_ext(authorities: Vec) -> runtime_io::TestExternalities; pub type Aura = Module; diff --git a/srml/aura/src/tests.rs b/srml/aura/src/tests.rs index 12deeb99a8d3e..a90eddf18f730 100644 --- a/srml/aura/src/tests.rs +++ b/srml/aura/src/tests.rs @@ -18,75 +18,13 @@ #![cfg(test)] -use lazy_static::lazy_static; -use crate::mock::{System, Aura, new_test_ext}; -use sr_primitives::traits::Header; use runtime_io::with_externalities; -use parking_lot::Mutex; -use crate::{AuraReport, HandleReport}; +use crate::mock::{Aura, new_test_ext}; #[test] -fn aura_report_gets_skipped_correctly() { - let mut report = AuraReport { - start_slot: 3, - skipped: 15, - }; - - let mut validators = vec![0; 10]; - report.punish(10, |idx, count| validators[idx] += count); - assert_eq!(validators, vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - - let mut validators = vec![0; 10]; - report.skipped = 5; - report.punish(10, |idx, count| validators[idx] += count); - assert_eq!(validators, vec![0, 0, 0, 1, 1, 1, 1, 1, 0, 0]); - - let mut validators = vec![0; 10]; - report.start_slot = 8; - report.punish(10, |idx, count| validators[idx] += count); - assert_eq!(validators, vec![1, 1, 1, 0, 0, 0, 0, 0, 1, 1]); - - let mut validators = vec![0; 4]; - report.start_slot = 1; - report.skipped = 3; - report.punish(4, |idx, count| validators[idx] += count); - assert_eq!(validators, vec![0, 1, 1, 1]); - - let mut validators = vec![0; 4]; - report.start_slot = 2; - report.punish(4, |idx, count| validators[idx] += count); - assert_eq!(validators, vec![1, 0, 1, 1]); -} - -#[test] -fn aura_reports_offline() { - lazy_static! { - static ref SLASH_COUNTS: Mutex> = Mutex::new(vec![0; 4]); - } - - struct HandleTestReport; - impl HandleReport for HandleTestReport { - fn handle_report(report: AuraReport) { - let mut counts = SLASH_COUNTS.lock(); - report.punish(counts.len(), |idx, count| counts[idx] += count); - } - } - +fn initial_values() { with_externalities(&mut new_test_ext(vec![0, 1, 2, 3]), || { - System::initialize(&1, &Default::default(), &Default::default(), &Default::default()); - let slot_duration = Aura::slot_duration(); - - Aura::on_timestamp_set::(5 * slot_duration, slot_duration); - let header = System::finalize(); - - // no slashing when last step was 0. - assert_eq!(SLASH_COUNTS.lock().as_slice(), &[0, 0, 0, 0]); - - System::initialize(&2, &header.hash(), &Default::default(), &Default::default()); - Aura::on_timestamp_set::(8 * slot_duration, slot_duration); - let _header = System::finalize(); - - // Steps 6 and 7 were skipped. - assert_eq!(SLASH_COUNTS.lock().as_slice(), &[0, 0, 1, 1]); + assert_eq!(Aura::last(), 0u64); + assert_eq!(Aura::authorities().len(), 4); }); } diff --git a/srml/babe/Cargo.toml b/srml/babe/Cargo.toml index c922deb605ad0..039ccea3df853 100644 --- a/srml/babe/Cargo.toml +++ b/srml/babe/Cargo.toml @@ -11,6 +11,7 @@ serde = { version = "1.0.93", optional = true } inherents = { package = "substrate-inherents", path = "../../core/inherents", default-features = false } rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } srml-support = { path = "../support", default-features = false } system = { package = "srml-system", path = "../system", default-features = false } timestamp = { package = "srml-timestamp", path = "../timestamp", default-features = false } @@ -31,6 +32,7 @@ std = [ "rstd/std", "srml-support/std", "sr-primitives/std", + "sr-staking-primitives/std", "system/std", "timestamp/std", "inherents/std", diff --git a/srml/babe/src/lib.rs b/srml/babe/src/lib.rs index b0db04d12c7c1..b58fb26b5a127 100644 --- a/srml/babe/src/lib.rs +++ b/srml/babe/src/lib.rs @@ -18,15 +18,22 @@ //! from VRF outputs and manages epoch transitions. #![cfg_attr(not(feature = "std"), no_std)] -#![forbid(unused_must_use, unsafe_code, unused_variables, dead_code)] +#![forbid(unused_must_use, unsafe_code, unused_variables)] + +// TODO: @marcio uncomment this when BabeEquivocation is integrated. +// #![forbid(dead_code)] pub use timestamp; use rstd::{result, prelude::*}; use srml_support::{decl_storage, decl_module, StorageValue, StorageMap, traits::FindAuthor, traits::Get}; use timestamp::{OnTimestampSet}; -use sr_primitives::{generic::DigestItem, ConsensusEngineId}; +use sr_primitives::{generic::DigestItem, ConsensusEngineId, Perbill}; use sr_primitives::traits::{IsMember, SaturatedConversion, Saturating, RandomnessBeacon}; +use sr_staking_primitives::{ + SessionIndex, + offence::{Offence, Kind}, +}; use sr_primitives::weights::SimpleDispatchInfo; #[cfg(feature = "std")] use timestamp::TimestampInherentData; @@ -282,6 +289,53 @@ impl session::ShouldEndSession for Module { } } +// TODO [slashing]: @marcio use this, remove the dead_code annotation. +/// A BABE equivocation offence report. +/// +/// When a validator released two or more blocks at the same slot. +#[allow(dead_code)] +struct BabeEquivocationOffence { + /// A babe slot number in which this incident happened. + slot: u64, + /// The session index in which the incident happened. + session_index: SessionIndex, + /// The size of the validator set at the time of the offence. + validator_set_count: u32, + /// The authority that produced the equivocation. + offender: FullIdentification, +} + +impl Offence for BabeEquivocationOffence { + const ID: Kind = *b"babe:equivocatio"; + type TimeSlot = u64; + + fn offenders(&self) -> Vec { + vec![self.offender.clone()] + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.slot + } + + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill { + // the formula is min((3k / n)^2, 1) + let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count); + // _ ^ 2 + x.square() + } +} + impl Module { /// Determine the BABE slot duration based on the Timestamp module configuration. pub fn slot_duration() -> T::Moment { diff --git a/srml/grandpa/Cargo.toml b/srml/grandpa/Cargo.toml index 67dca3e15a1e6..71e43e8692ff1 100644 --- a/srml/grandpa/Cargo.toml +++ b/srml/grandpa/Cargo.toml @@ -12,6 +12,7 @@ substrate-finality-grandpa-primitives = { path = "../../core/finality-grandpa/pr rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false, features = [ "wasm-nice-panic-message" ] } sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } srml-support = { path = "../support", default-features = false } system = { package = "srml-system", path = "../system", default-features = false } session = { package = "srml-session", path = "../session", default-features = false } @@ -30,6 +31,7 @@ std = [ "rstd/std", "srml-support/std", "sr-primitives/std", + "sr-staking-primitives/std", "system/std", "session/std", "finality-tracker/std", diff --git a/srml/grandpa/src/lib.rs b/srml/grandpa/src/lib.rs index fe60e58d2e355..d64939ae0a766 100644 --- a/srml/grandpa/src/lib.rs +++ b/srml/grandpa/src/lib.rs @@ -37,6 +37,11 @@ use srml_support::{ }; use sr_primitives::{ generic::{DigestItem, OpaqueDigestItemId}, traits::Zero, + Perbill, +}; +use sr_staking_primitives::{ + SessionIndex, + offence::{Offence, Kind}, }; use fg_primitives::{ScheduledChange, ConsensusLog, GRANDPA_ENGINE_ID}; pub use fg_primitives::{AuthorityId, AuthorityWeight}; @@ -402,3 +407,56 @@ impl finality_tracker::OnFinalizationStalled for Modul >::put((further_wait, median)); } } + +/// A round number and set id which point on the time of an offence. +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)] +struct GrandpaTimeSlot { + // The order of these matters for `derive(Ord)`. + set_id: u64, + round: u64, +} + +// TODO [slashing]: Integrate this. +/// A grandpa equivocation offence report. +#[allow(dead_code)] +struct GrandpaEquivocationOffence { + /// Time slot at which this incident happened. + time_slot: GrandpaTimeSlot, + /// The session index in which the incident happened. + session_index: SessionIndex, + /// The size of the validator set at the time of the offence. + validator_set_count: u32, + /// The authority which produced this equivocation. + offender: FullIdentification, +} + +impl Offence for GrandpaEquivocationOffence { + const ID: Kind = *b"grandpa:equivoca"; + type TimeSlot = GrandpaTimeSlot; + + fn offenders(&self) -> Vec { + vec![self.offender.clone()] + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.time_slot + } + + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill { + // the formula is min((3k / n)^2, 1) + let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count); + // _ ^ 2 + x.square() + } +} diff --git a/srml/grandpa/src/tests.rs b/srml/grandpa/src/tests.rs index adef602ce6f18..41229a5136165 100644 --- a/srml/grandpa/src/tests.rs +++ b/srml/grandpa/src/tests.rs @@ -282,3 +282,31 @@ fn schedule_resume_only_when_paused() { ); }); } + +#[test] +fn time_slot_have_sane_ord() { + // Ensure that `Ord` implementation is sane. + const FIXTURE: &[GrandpaTimeSlot] = &[ + GrandpaTimeSlot { + set_id: 0, + round: 0, + }, + GrandpaTimeSlot { + set_id: 0, + round: 1, + }, + GrandpaTimeSlot { + set_id: 1, + round: 0, + }, + GrandpaTimeSlot { + set_id: 1, + round: 1, + }, + GrandpaTimeSlot { + set_id: 1, + round: 2, + } + ]; + assert!(FIXTURE.windows(2).all(|f| f[0] < f[1])); +} diff --git a/srml/im-online/Cargo.toml b/srml/im-online/Cargo.toml index 8f0e4cc39fa82..5f7514755b72f 100644 --- a/srml/im-online/Cargo.toml +++ b/srml/im-online/Cargo.toml @@ -5,27 +5,30 @@ authors = ["Parity Technologies "] edition = "2018" [dependencies] -codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } -sr-primitives = { path = "../../core/sr-primitives", default-features = false } -primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false } app-crypto = { package = "substrate-application-crypto", path = "../../core/application-crypto", default-features = false } +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } +primitives = { package="substrate-primitives", path = "../../core/primitives", default-features = false } rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } serde = { version = "1.0", optional = true } session = { package = "srml-session", path = "../session", default-features = false } +sr-io = { path = "../../core/sr-io", default-features = false } +sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } srml-support = { path = "../support", default-features = false } -sr-io = { package = "sr-io", path = "../../core/sr-io", default-features = false } system = { package = "srml-system", path = "../system", default-features = false } [features] -default = ["std"] +default = ["std", "session/historical"] std = [ + "app-crypto/std", "codec/std", - "sr-primitives/std", + "primitives/std", "rstd/std", "serde", "session/std", - "srml-support/std", "sr-io/std", + "sr-primitives/std", + "sr-staking-primitives/std", + "srml-support/std", "system/std", - "app-crypto/std", ] diff --git a/srml/im-online/src/lib.rs b/srml/im-online/src/lib.rs index 29811f8f926bf..628531c9f318f 100644 --- a/srml/im-online/src/lib.rs +++ b/srml/im-online/src/lib.rs @@ -67,20 +67,25 @@ // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] -use primitives::offchain::{OpaqueNetworkState, StorageKind}; +use app_crypto::RuntimeAppPublic; use codec::{Encode, Decode}; +use primitives::offchain::{OpaqueNetworkState, StorageKind}; +use rstd::prelude::*; +use session::historical::IdentificationTuple; +use sr_io::Printable; use sr_primitives::{ - ApplyError, traits::Extrinsic as ExtrinsicT, + Perbill, ApplyError, + traits::{Extrinsic as ExtrinsicT, Convert}, transaction_validity::{TransactionValidity, TransactionLongevity, ValidTransaction}, }; -use rstd::prelude::*; -use session::SessionIndex; -use sr_io::Printable; +use sr_staking_primitives::{ + SessionIndex, CurrentElectedSet, + offence::{ReportOffence, Offence, Kind}, +}; use srml_support::{ StorageValue, decl_module, decl_event, decl_storage, StorageDoubleMap, print, ensure }; use system::ensure_none; -use app_crypto::RuntimeAppPublic; mod app { pub use app_crypto::sr25519 as crypto; @@ -152,7 +157,7 @@ pub struct Heartbeat authority_index: AuthIndex, } -pub trait Trait: system::Trait + session::Trait { +pub trait Trait: system::Trait + session::historical::Trait { /// The overarching event type. type Event: From + Into<::Event>; @@ -162,6 +167,17 @@ pub trait Trait: system::Trait + session::Trait { /// A extrinsic right from the external world. This is unchecked and so /// can contain a signature. type UncheckedExtrinsic: ExtrinsicT::Call> + Encode + Decode; + + /// A type that gives us the ability to submit unresponsiveness offence reports. + type ReportUnresponsiveness: + ReportOffence< + Self::AccountId, + IdentificationTuple, + UnresponsivenessOffence>, + >; + + /// A type that returns a validator id from the current elected set of the era. + type CurrentElectedSet: CurrentElectedSet<::ValidatorId>; } decl_event!( @@ -382,6 +398,7 @@ impl Module { } impl session::OneSessionHandler for Module { + type Key = AuthorityId; fn on_genesis_session<'a, I: 'a>(validators: I) @@ -404,6 +421,44 @@ impl session::OneSessionHandler for Module { Keys::put(validators.map(|x| x.1).collect::>()); } + fn on_before_session_ending() { + let mut unresponsive = vec![]; + + let current_session = >::current_index(); + + let keys = Keys::get(); + let current_elected = T::CurrentElectedSet::current_elected_set(); + + // The invariant is that these two are of the same length. + // TODO: What to do: Uncomment, ignore, a third option? + // assert_eq!(keys.len(), current_elected.len()); + + for (auth_idx, validator_id) in current_elected.into_iter().enumerate() { + let auth_idx = auth_idx as u32; + if !::exists(¤t_session, &auth_idx) { + let full_identification = T::FullIdentificationOf::convert(validator_id.clone()) + .expect( + "we got the validator_id from current_elected; + current_elected is set of currently elected validators; + the mapping between the validator id and its full identification should be valid; + thus `FullIdentificationOf::convert` can't return `None`; + qed", + ); + + unresponsive.push((validator_id, full_identification)); + } + } + + let validator_set_count = keys.len() as u32; + let offence = UnresponsivenessOffence { + session_index: current_session, + validator_set_count, + offenders: unresponsive, + }; + + T::ReportUnresponsiveness::report_offence(vec![], offence); + } + fn on_disabled(_i: usize) { // ignore } @@ -453,3 +508,74 @@ impl srml_support::unsigned::ValidateUnsigned for Module { TransactionValidity::Invalid(0) } } + +/// An offence that is filed if a validator didn't send a heartbeat message. +pub struct UnresponsivenessOffence { + /// The current session index in which we report the unresponsive validators. + /// + /// It acts as a time measure for unresponsiveness reports and effectively will always point + /// at the end of the session. + session_index: SessionIndex, + /// The size of the validator set in current session/era. + validator_set_count: u32, + /// Authorities that were unresponsive during the current era. + offenders: Vec, +} + +impl Offence for UnresponsivenessOffence { + const ID: Kind = *b"im-online:offlin"; + type TimeSlot = SessionIndex; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.session_index + } + + fn slash_fraction(offenders: u32, validator_set_count: u32) -> Perbill { + // the formula is min((3 * (k - 1)) / n, 1) * 0.05 + let x = Perbill::from_rational_approximation(3 * (offenders - 1), validator_set_count); + + // _ * 0.05 + // For now, Perbill doesn't support multiplication other than an integer so we perform + // a manual scaling. + // TODO: #3189 should fix this. + let p = (x.into_parts() as u64 * 50_000_000u64) / 1_000_000_000u64; + Perbill::from_parts(p as u32) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unresponsiveness_slash_fraction() { + // A single case of unresponsiveness is not slashed. + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(1, 50), + Perbill::zero(), + ); + + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(3, 50), + Perbill::from_parts(6000000), // 0.6% + ); + + // One third offline should be punished around 5%. + assert_eq!( + UnresponsivenessOffence::<()>::slash_fraction(17, 50), + Perbill::from_parts(48000000), // 4.8% + ); + } +} diff --git a/srml/offences/Cargo.toml b/srml/offences/Cargo.toml new file mode 100644 index 0000000000000..4dd12413f6729 --- /dev/null +++ b/srml/offences/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "srml-offences" +version = "1.0.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +balances = { package = "srml-balances", path = "../balances", default-features = false } +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } +rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } +serde = { version = "1.0", optional = true } +sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } +support = { package = "srml-support", path = "../support", default-features = false } +system = { package = "srml-system", path = "../system", default-features = false } + +[dev-dependencies] +runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false } +substrate-primitives = { path = "../../core/primitives" } + +[features] +default = ["std"] +std = [ + "balances/std", + "codec/std", + "rstd/std", + "serde", + "sr-primitives/std", + "sr-staking-primitives/std", + "support/std", + "system/std", +] diff --git a/srml/offences/src/lib.rs b/srml/offences/src/lib.rs new file mode 100644 index 0000000000000..a89efa2f830c4 --- /dev/null +++ b/srml/offences/src/lib.rs @@ -0,0 +1,282 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! # Offences Module +//! +//! Tracks reported offences + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod mock; +mod tests; + +use rstd::{ + vec::Vec, + collections::btree_set::BTreeSet, +}; +use support::{ + StorageMap, StorageDoubleMap, decl_module, decl_event, decl_storage, Parameter, +}; +use sr_primitives::{ + Perbill, + traits::Hash, +}; +use sr_staking_primitives::{ + offence::{Offence, ReportOffence, Kind, OnOffenceHandler, OffenceDetails}, +}; +use codec::{Encode, Decode}; + +/// A binary blob which represents a SCALE codec-encoded `O::TimeSlot`. +type OpaqueTimeSlot = Vec; + +/// A type alias for a report identifier. +type ReportIdOf = ::Hash; + +/// Offences trait +pub trait Trait: system::Trait { + /// The overarching event type. + type Event: From + Into<::Event>; + /// Full identification of the validator. + type IdentificationTuple: Parameter + Ord; + /// A handler called for every offence report. + type OnOffenceHandler: OnOffenceHandler; +} + +decl_storage! { + trait Store for Module as Offences { + /// The primary structure that holds all offence records keyed by report identifiers. + Reports get(reports): map ReportIdOf => Option>; + + /// A vector of reports of the same kind that happened at the same time slot. + ConcurrentReportsIndex: double_map Kind, blake2_256(OpaqueTimeSlot) => Vec>; + + /// Enumerates all reports of a kind along with the time they happened. + /// + /// All reports are sorted by the time of offence. + /// + /// Note that the actual type of this mapping is `Vec`, this is because values of + /// different types are not supported at the moment so we are doing the manual serialization. + ReportsByKindIndex: map Kind => Vec; // (O::TimeSlot, ReportIdOf) + } +} + +decl_event!( + pub enum Event { + /// There is an offence reported of the given `kind` happened at the `session_index` and + /// (kind-specific) time slot. This event is not deposited for duplicate slashes. + Offence(Kind, OpaqueTimeSlot), + } +); + +decl_module! { + /// Offences module, currently just responsible for taking offence reports. + pub struct Module for enum Call where origin: T::Origin { + fn deposit_event() = default; + } +} +impl> + ReportOffence for Module +where + T::IdentificationTuple: Clone, +{ + fn report_offence(reporters: Vec, offence: O) { + let offenders = offence.offenders(); + let time_slot = offence.time_slot(); + let validator_set_count = offence.validator_set_count(); + + // Go through all offenders in the offence report and find all offenders that was spotted + // in unique reports. + let TriageOutcome { + new_offenders, + concurrent_offenders, + } = match Self::triage_offence_report::(reporters, &time_slot, offenders) { + Some(triage) => triage, + // The report contained only duplicates, so there is no need to slash again. + None => return, + }; + + // Deposit the event. + Self::deposit_event(Event::Offence(O::ID, time_slot.encode())); + + let offenders_count = concurrent_offenders.len() as u32; + let previous_offenders_count = offenders_count - new_offenders.len() as u32; + + // The amount new offenders are slashed + let new_fraction = O::slash_fraction(offenders_count, validator_set_count); + + // The amount previous offenders are slashed additionally. + // + // Since they were slashed in the past, we slash by: + // x = (new - prev) / (1 - prev) + // because: + // Y = X * (1 - prev) + // Z = Y * (1 - x) + // Z = X * (1 - new) + let old_fraction = if previous_offenders_count > 0 { + let previous_fraction = O::slash_fraction( + offenders_count.saturating_sub(previous_offenders_count), + validator_set_count, + ); + let numerator = new_fraction + .into_parts() + .saturating_sub(previous_fraction.into_parts()); + let denominator = + Perbill::from_parts(Perbill::one().into_parts() - previous_fraction.into_parts()); + Perbill::from_parts(denominator * numerator) + } else { + new_fraction.clone() + }; + + // calculate how much to slash + let slash_perbill = concurrent_offenders + .iter() + .map(|details| { + if previous_offenders_count > 0 && new_offenders.contains(&details.offender) { + new_fraction.clone() + } else { + old_fraction.clone() + } + }) + .collect::>(); + + T::OnOffenceHandler::on_offence(&concurrent_offenders, &slash_perbill); + } +} + +impl Module { + /// Compute the ID for the given report properties. + /// + /// The report id depends on the offence kind, time slot and the id of offender. + fn report_id>( + time_slot: &O::TimeSlot, + offender: &T::IdentificationTuple, + ) -> ReportIdOf { + (O::ID, time_slot.encode(), offender).using_encoded(T::Hashing::hash) + } + + /// Triages the offence report and returns the set of offenders that was involved in unique + /// reports along with the list of the concurrent offences. + fn triage_offence_report>( + reporters: Vec, + time_slot: &O::TimeSlot, + offenders: Vec, + ) -> Option> { + let mut storage = ReportIndexStorage::::load(time_slot); + let mut new_offenders = BTreeSet::new(); + + for offender in offenders { + let report_id = Self::report_id::(time_slot, &offender); + + if !>::exists(&report_id) { + new_offenders.insert(offender.clone()); + >::insert( + &report_id, + OffenceDetails { + offender, + reporters: reporters.clone(), + }, + ); + + storage.insert(time_slot, report_id); + } + } + + if !new_offenders.is_empty() { + // Load report details for the all reports happened at the same time. + let concurrent_offenders = storage.concurrent_reports + .iter() + .filter_map(|report_id| >::get(report_id)) + .collect::>(); + + storage.save(); + + Some(TriageOutcome { + new_offenders, + concurrent_offenders, + }) + } else { + None + } + } +} + +struct TriageOutcome { + /// Offenders that was spotted in the unique reports. + new_offenders: BTreeSet, + /// Other reports for the same report kinds. + concurrent_offenders: Vec>, +} + +/// An auxilary struct for working with storage of indexes localized for a specific offence +/// kind (specified by the `O` type parameter). +/// +/// This struct is responsible for aggregating storage writes and the underlying storage should not +/// accessed directly meanwhile. +#[must_use = "The changes are not saved without called `save`"] +struct ReportIndexStorage> { + opaque_time_slot: OpaqueTimeSlot, + concurrent_reports: Vec>, + same_kind_reports: Vec<(O::TimeSlot, ReportIdOf)>, +} + +impl> ReportIndexStorage { + /// Preload indexes from the storage for the specific `time_slot` and the kind of the offence. + fn load(time_slot: &O::TimeSlot) -> Self { + let opaque_time_slot = time_slot.encode(); + + let same_kind_reports = ::get(&O::ID); + let same_kind_reports = + Vec::<(O::TimeSlot, ReportIdOf)>::decode(&mut &same_kind_reports[..]) + .unwrap_or_default(); + + let concurrent_reports = >::get(&O::ID, &opaque_time_slot); + + Self { + opaque_time_slot, + concurrent_reports, + same_kind_reports, + } + } + + /// Insert a new report to the index. + fn insert(&mut self, time_slot: &O::TimeSlot, report_id: ReportIdOf) { + // Insert the report id into the list while maintaining the ordering by the time + // slot. + let pos = match self + .same_kind_reports + .binary_search_by_key(&time_slot, |&(ref when, _)| when) + { + Ok(pos) => pos, + Err(pos) => pos, + }; + self.same_kind_reports + .insert(pos, (time_slot.clone(), report_id)); + + // Update the list of concurrent reports. + self.concurrent_reports.push(report_id); + } + + /// Dump the indexes to the storage. + fn save(self) { + ::insert(&O::ID, self.same_kind_reports.encode()); + >::insert( + &O::ID, + &self.opaque_time_slot, + &self.concurrent_reports, + ); + } +} diff --git a/srml/offences/src/mock.rs b/srml/offences/src/mock.rs new file mode 100644 index 0000000000000..78355f642ef76 --- /dev/null +++ b/srml/offences/src/mock.rs @@ -0,0 +1,162 @@ +// Copyright 2018-2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Test utilities + +#![cfg(test)] + +use std::cell::RefCell; +use crate::{Module, Trait}; +use codec::Encode; +use sr_primitives::Perbill; +use sr_staking_primitives::{ + SessionIndex, + offence::{self, Kind, OffenceDetails}, +}; +use sr_primitives::testing::Header; +use sr_primitives::traits::{IdentityLookup, BlakeTwo256}; +use substrate_primitives::{H256, Blake2Hasher}; +use support::{impl_outer_origin, impl_outer_event, parameter_types, StorageMap, StorageDoubleMap}; +use {runtime_io, system}; + +impl_outer_origin!{ + pub enum Origin for Runtime {} +} + +pub struct OnOffenceHandler; + +thread_local! { + pub static ON_OFFENCE_PERBILL: RefCell> = RefCell::new(Default::default()); +} + +impl offence::OnOffenceHandler for OnOffenceHandler { + fn on_offence( + _offenders: &[OffenceDetails], + slash_fraction: &[Perbill], + ) { + ON_OFFENCE_PERBILL.with(|f| { + *f.borrow_mut() = slash_fraction.to_vec(); + }); + } +} + +pub fn with_on_offence_fractions) -> R>(f: F) -> R { + ON_OFFENCE_PERBILL.with(|fractions| { + f(&mut *fractions.borrow_mut()) + }) +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} +impl system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type WeightMultiplierUpdate = (); + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; +} + +impl Trait for Runtime { + type Event = TestEvent; + type IdentificationTuple = u64; + type OnOffenceHandler = OnOffenceHandler; +} + +mod offences { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Runtime { + offences, + } +} + +pub fn new_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default().build_storage::().unwrap(); + t.into() +} + +/// Offences module. +pub type Offences = Module; +pub type System = system::Module; + +pub const KIND: [u8; 16] = *b"test_report_1234"; + +/// Returns all offence details for the specific `kind` happened at the specific time slot. +pub fn offence_reports(kind: Kind, time_slot: u128) -> Vec> { + >::get(&kind, &time_slot.encode()) + .into_iter() + .map(|report_id| { + >::get(&report_id) + .expect("dangling report id is found in ConcurrentReportsIndex") + }) + .collect() +} + +#[derive(Clone)] +pub struct Offence { + pub validator_set_count: u32, + pub offenders: Vec, + pub time_slot: u128, +} + +impl offence::Offence for Offence { + const ID: offence::Kind = KIND; + type TimeSlot = u128; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> u128 { + self.time_slot + } + + fn session_index(&self) -> SessionIndex { + // session index is not used by the srml-offences directly, but rather it exists only for + // filtering historical reports. + unimplemented!() + } + + fn slash_fraction( + offenders_count: u32, + validator_set_count: u32, + ) -> Perbill { + Perbill::from_percent(5 + offenders_count * 100 / validator_set_count) + } +} diff --git a/srml/offences/src/tests.rs b/srml/offences/src/tests.rs new file mode 100644 index 0000000000000..17f933b8e8d12 --- /dev/null +++ b/srml/offences/src/tests.rs @@ -0,0 +1,246 @@ +// Copyright 2017-2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Tests for the offences module. + +#![cfg(test)] + +use super::*; +use crate::mock::{ + Offences, System, Offence, TestEvent, KIND, new_test_ext, with_on_offence_fractions, + offence_reports, +}; +use system::{EventRecord, Phase}; +use runtime_io::with_externalities; + +#[test] +fn should_report_an_authority_and_trigger_on_offence() { + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + + // when + Offences::report_offence(vec![], offence); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + }); + }); +} + +#[test] +fn should_calculate_the_fraction_correctly() { + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + let offence1 = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + let offence2 = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![4], + }; + + // when + Offences::report_offence(vec![], offence1); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + }); + + Offences::report_offence(vec![], offence2); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(15), Perbill::from_percent(45)]); + }); + }); +} + +#[test] +fn should_not_report_the_same_authority_twice_in_the_same_slot() { + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + Offences::report_offence(vec![], offence.clone()); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + Offences::report_offence(vec![], offence); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![]); + }); + }); +} + + +#[test] +fn should_report_in_different_time_slot() { + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let mut offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + Offences::report_offence(vec![], offence.clone()); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // reportfor the second time + offence.time_slot += 1; + Offences::report_offence(vec![], offence); + + // then + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + }); + }); +} + +#[test] +fn should_deposit_event() { + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + + // when + Offences::report_offence(vec![], offence); + + // then + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::offences(crate::Event::Offence(KIND, time_slot.encode())), + topics: vec![], + }] + ); + }); +} + +#[test] +fn doesnt_deposit_event_for_dups() { + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + Offences::report_offence(vec![], offence.clone()); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + Offences::report_offence(vec![], offence); + + // then + // there is only one event. + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: TestEvent::offences(crate::Event::Offence(KIND, time_slot.encode())), + topics: vec![], + }] + ); + }); +} + +#[test] +fn should_properly_count_offences() { + // We report two different authorities for the same issue. Ultimately, the 1st authority + // should have `count` equal 2 and the count of the 2nd one should be equal to 1. + with_externalities(&mut new_test_ext(), || { + // given + let time_slot = 42; + assert_eq!(offence_reports(KIND, time_slot), vec![]); + + let offence1 = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![5], + }; + let offence2 = Offence { + validator_set_count: 5, + time_slot, + offenders: vec![4], + }; + Offences::report_offence(vec![], offence1); + with_on_offence_fractions(|f| { + assert_eq!(f.clone(), vec![Perbill::from_percent(25)]); + f.clear(); + }); + + // when + // report for the second time + Offences::report_offence(vec![], offence2); + + // then + // the 1st authority should have count 2 and the 2nd one should be reported only once. + assert_eq!( + offence_reports(KIND, time_slot), + vec![ + OffenceDetails { offender: 5, reporters: vec![] }, + OffenceDetails { offender: 4, reporters: vec![] }, + ] + ); + }); +} diff --git a/srml/session/Cargo.toml b/srml/session/Cargo.toml index d084ab4261b3c..d0166997656ed 100644 --- a/srml/session/Cargo.toml +++ b/srml/session/Cargo.toml @@ -10,6 +10,7 @@ safe-mix = { version = "1.0", default-features = false} codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } srml-support = { path = "../support", default-features = false } system = { package = "srml-system", path = "../system", default-features = false } timestamp = { package = "srml-timestamp", path = "../timestamp", default-features = false } @@ -31,6 +32,7 @@ std = [ "rstd/std", "srml-support/std", "sr-primitives/std", + "sr-staking-primitives/std", "timestamp/std", "substrate-trie/std" ] diff --git a/srml/session/src/historical.rs b/srml/session/src/historical.rs index c0ffdeb464eb3..7e4686e92327f 100644 --- a/srml/session/src/historical.rs +++ b/srml/session/src/historical.rs @@ -37,6 +37,8 @@ use substrate_trie::{MemoryDB, Trie, TrieMut, Recorder, EMPTY_PREFIX}; use substrate_trie::trie_types::{TrieDBMut, TrieDB}; use super::{SessionIndex, Module as SessionModule}; +type ValidatorCount = u32; + /// Trait necessary for the historical module. pub trait Trait: super::Trait { /// Full identification of the validator. @@ -55,8 +57,8 @@ pub trait Trait: super::Trait { decl_storage! { trait Store for Module as Session { - /// Mapping from historical session indices to session-data root hash. - HistoricalSessions get(historical_root): map SessionIndex => Option; + /// Mapping from historical session indices to session-data root hash and validator count. + HistoricalSessions get(historical_root): map SessionIndex => Option<(T::Hash, ValidatorCount)>; /// Queued full identifications for queued sessions whose validators have become obsolete. CachedObsolete get(cached_obsolete): map SessionIndex => Option>; @@ -121,8 +123,9 @@ impl crate::OnSessionEnding for NoteHistoricalRoot< // do all of this _before_ calling the other `on_session_ending` impl // so that we have e.g. correct exposures from the _current_. + let count = >::validators().len() as u32; match ProvingTrie::::generate_for(ending) { - Ok(trie) => >::insert(ending, &trie.root), + Ok(trie) => >::insert(ending, &(trie.root, count)), Err(reason) => { print("Failed to generate historical ancestry-inclusion proof."); print(reason); @@ -278,7 +281,7 @@ impl> srml_support::traits::KeyOwnerProofSystem<(KeyTyp for Module { type Proof = Proof; - type FullIdentification = IdentificationTuple; + type IdentificationTuple = IdentificationTuple; fn prove(key: (KeyTypeId, D)) -> Option { let session = >::current_index(); @@ -300,7 +303,7 @@ impl> srml_support::traits::KeyOwnerProofSystem<(KeyTyp T::FullIdentificationOf::convert(owner.clone()).map(move |id| (owner, id)) ) } else { - let root = >::get(&proof.session)?; + let (root, _) = >::get(&proof.session)?; let trie = ProvingTrie::::from_nodes(root, &proof.trie_nodes); trie.query(id, data.as_ref()) diff --git a/srml/session/src/lib.rs b/srml/session/src/lib.rs index 800ccc2e7eb7b..f8e13d529b875 100644 --- a/srml/session/src/lib.rs +++ b/srml/session/src/lib.rs @@ -124,6 +124,7 @@ use codec::Decode; use sr_primitives::{KeyTypeId, AppKey}; use sr_primitives::weights::SimpleDispatchInfo; use sr_primitives::traits::{Convert, Zero, Member, OpaqueKeys}; +use sr_staking_primitives::SessionIndex; use srml_support::{ dispatch::Result, ConsensusEngineId, StorageValue, StorageDoubleMap, for_each_tuple, decl_module, decl_event, decl_storage, @@ -137,9 +138,6 @@ mod mock; #[cfg(feature = "historical")] pub mod historical; -/// Simple index type with which we can count sessions. -pub type SessionIndex = u32; - /// Decides whether the session should be ended. pub trait ShouldEndSession { /// Return `true` if the session should be ended. @@ -168,6 +166,7 @@ impl< } /// An event handler for when the session is ending. +/// TODO [slashing] consider renaming to OnSessionStarting pub trait OnSessionEnding { /// Handle the fact that the session is ending, and optionally provide the new validator set. /// @@ -185,7 +184,7 @@ impl OnSessionEnding for () { fn on_session_ending(_: SessionIndex, _: SessionIndex) -> Option> { None } } -/// Handler for when a session keys set changes. +/// Handler for session lifecycle events. pub trait SessionHandler { /// The given validator set will be used for the genesis session. /// It is guaranteed that the given validator set will also be used @@ -200,11 +199,17 @@ pub trait SessionHandler { queued_validators: &[(ValidatorId, Ks)], ); + /// A notification for end of the session. + /// + /// Note it is triggered before any `OnSessionEnding` handlers, + /// so we can still affect the validator set. + fn on_before_session_ending() {} + /// A validator got disabled. Act accordingly until a new session begins. fn on_disabled(validator_index: usize); } -/// One session-key type handler. +/// A session handler for specific key type. pub trait OneSessionHandler { /// The key type expected. type Key: Decode + Default + AppKey; @@ -212,10 +217,23 @@ pub trait OneSessionHandler { fn on_genesis_session<'a, I: 'a>(validators: I) where I: Iterator, ValidatorId: 'a; - fn on_new_session<'a, I: 'a>(changed: bool, validators: I, queued_validators: I) - where I: Iterator, ValidatorId: 'a; + /// Session set has changed; act appropriately. + fn on_new_session<'a, I: 'a>( + _changed: bool, + _validators: I, + _queued_validators: I + ) where I: Iterator, ValidatorId: 'a; + + + /// A notification for end of the session. + /// + /// Note it is triggered before any `OnSessionEnding` handlers, + /// so we can still affect the validator set. + fn on_before_session_ending() {} + + /// A validator got disabled. Act accordingly until a new session begins. + fn on_disabled(_validator_index: usize); - fn on_disabled(i: usize); } macro_rules! impl_session_handlers { @@ -223,6 +241,7 @@ macro_rules! impl_session_handlers { impl SessionHandler for () { fn on_genesis_session(_: &[(AId, Ks)]) {} fn on_new_session(_: bool, _: &[(AId, Ks)], _: &[(AId, Ks)]) {} + fn on_before_session_ending() {} fn on_disabled(_: usize) {} } ); @@ -253,6 +272,13 @@ macro_rules! impl_session_handlers { $t::on_new_session(changed, our_keys, queued_keys); )* } + + fn on_before_session_ending() { + $( + $t::on_before_session_ending(); + )* + } + fn on_disabled(i: usize) { $( $t::on_disabled(i); diff --git a/srml/session/src/mock.rs b/srml/session/src/mock.rs index 74380426beb15..82fefc39584d8 100644 --- a/srml/session/src/mock.rs +++ b/srml/session/src/mock.rs @@ -24,6 +24,7 @@ use sr_primitives::{ Perbill, impl_opaque_keys, traits::{BlakeTwo256, IdentityLookup, ConvertInto}, testing::{Header, UintAuthorityId} }; +use sr_staking_primitives::SessionIndex; impl_opaque_keys! { pub struct MockSessionKeys { diff --git a/srml/staking/Cargo.toml b/srml/staking/Cargo.toml index 5428d663f3f44..d985730912c56 100644 --- a/srml/staking/Cargo.toml +++ b/srml/staking/Cargo.toml @@ -12,6 +12,7 @@ substrate-keyring = { path = "../../core/keyring", optional = true } rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false } runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false } sr-primitives = { path = "../../core/sr-primitives", default-features = false } +sr-staking-primitives = { path = "../../core/sr-staking-primitives", default-features = false } srml-support = { path = "../support", default-features = false } system = { package = "srml-system", path = "../system", default-features = false } session = { package = "srml-session", path = "../session", default-features = false, features = ["historical"] } @@ -36,6 +37,7 @@ std = [ "runtime_io/std", "srml-support/std", "sr-primitives/std", + "sr-staking-primitives/std", "session/std", "system/std", "authorship/std", diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index 9a3424735eff8..e7bb42c64dfe5 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -133,7 +133,7 @@ //! //! ## Usage //! -//! ### Example: Reporting Misbehavior +//! ### Example: Rewarding a validator by id. //! //! ``` //! use srml_support::{decl_module, dispatch::Result}; @@ -144,10 +144,10 @@ //! //! decl_module! { //! pub struct Module for enum Call where origin: T::Origin { -//! /// Report whoever calls this function as offline once. -//! pub fn report_sender(origin) -> Result { +//! /// Reward a validator. +//! pub fn reward_myself(origin) -> Result { //! let reported = ensure_signed(origin)?; -//! >::on_offline_validator(reported, 1); +//! >::reward_by_ids(vec![(reported, 10)]); //! Ok(()) //! } //! } @@ -203,28 +203,6 @@ //! - Stash account, not increasing the staked value. //! - Stash account, also increasing the staked value. //! -//! ### Slashing details -//! -//! A validator can be _reported_ to be offline at any point via the public function -//! [`on_offline_validator`](enum.Call.html#variant.on_offline_validator). Each validator declares -//! how many times it can be _reported_ before it actually gets slashed via its -//! [`ValidatorPrefs::unstake_threshold`](./struct.ValidatorPrefs.html#structfield.unstake_threshold). -//! -//! On top of this, the Staking module also introduces an -//! [`OfflineSlashGrace`](./struct.Module.html#method.offline_slash_grace), which applies -//! to all validators and prevents them from getting immediately slashed. -//! -//! Essentially, a validator gets slashed once they have been reported more than -//! [`OfflineSlashGrace`] + [`ValidatorPrefs::unstake_threshold`] times. Getting slashed due to -//! offline report always leads to being _unstaked_ (_i.e._ removed as a validator candidate) as -//! the consequence. -//! -//! The base slash value is computed _per slash-event_ by multiplying -//! [`OfflineSlash`](./struct.Module.html#method.offline_slash) and the `total` `Exposure`. This -//! value is then multiplied by `2.pow(unstake_threshold)` to obtain the final slash value. All -//! individual accounts' punishments are capped at their total stake (NOTE: This cap should never -//! come into force in a correctly implemented, non-corrupted, well-configured system). -//! //! ### Additional Fund Management Operations //! //! Any funds already placed into stash can be the target of the following operations: @@ -293,12 +271,16 @@ use srml_support::{ WithdrawReasons, WithdrawReason, OnUnbalanced, Imbalance, Get, Time } }; -use session::{historical::OnSessionEnding, SelectInitialValidators, SessionIndex}; +use session::{historical::OnSessionEnding, SelectInitialValidators}; use sr_primitives::Perbill; use sr_primitives::weights::SimpleDispatchInfo; use sr_primitives::traits::{ - Convert, Zero, One, StaticLookup, CheckedSub, CheckedShl, Saturating, Bounded, - SaturatedConversion, SimpleArithmetic + Convert, Zero, One, StaticLookup, CheckedSub, Saturating, Bounded, + SimpleArithmetic, SaturatedConversion, +}; +use sr_staking_primitives::{ + SessionIndex, CurrentElectedSet, + offence::{OnOffenceHandler, OffenceDetails, Offence, ReportOffence}, }; #[cfg(feature = "std")] use sr_primitives::{Serialize, Deserialize}; @@ -306,10 +288,8 @@ use system::{ensure_signed, ensure_root}; use phragmen::{elect, ACCURACY, ExtendedBalance, equalize}; -const RECENT_OFFLINE_COUNT: usize = 32; const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4; const MAX_NOMINATIONS: usize = 16; -const MAX_UNSTAKE_THRESHOLD: u32 = 10; const MAX_UNLOCKING_CHUNKS: usize = 32; const STAKING_ID: LockIdentifier = *b"staking "; @@ -371,9 +351,6 @@ impl Default for RewardDestination { #[derive(PartialEq, Eq, Clone, Encode, Decode)] #[cfg_attr(feature = "std", derive(Debug))] pub struct ValidatorPrefs { - /// Validator should ensure this many more slashes than is necessary before being unstaked. - #[codec(compact)] - pub unstake_threshold: u32, /// Reward that validator takes up-front; only the rest is split between themselves and /// nominators. #[codec(compact)] @@ -383,7 +360,6 @@ pub struct ValidatorPrefs { impl Default for ValidatorPrefs { fn default() -> Self { ValidatorPrefs { - unstake_threshold: 3, validator_payment: Default::default(), } } @@ -465,6 +441,15 @@ pub struct Exposure { pub others: Vec>, } +/// A slashing event occurred, slashing a validator for a given amount of balance. +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct SlashJournalEntry { + who: AccountId, + amount: Balance, + own_slash: Balance, // the amount of `who`'s own exposure that was slashed +} + pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type PositiveImbalanceOf = @@ -492,7 +477,7 @@ pub trait SessionInterface: system::Trait { /// Get the validators from session. fn validators() -> Vec; /// Prune historical session tries up to but not including the given index. - fn prune_historical_up_to(up_to: session::SessionIndex); + fn prune_historical_up_to(up_to: SessionIndex); } impl SessionInterface<::AccountId> for T where @@ -514,7 +499,7 @@ impl SessionInterface<::AccountId> for T where >::validators() } - fn prune_historical_up_to(up_to: session::SessionIndex) { + fn prune_historical_up_to(up_to: SessionIndex) { >::prune_up_to(up_to); } } @@ -579,10 +564,6 @@ decl_storage! { /// Minimum number of staking participants before emergency conditions are imposed. pub MinimumValidatorCount get(minimum_validator_count) config(): u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT; - /// Slash, per validator that is taken for the first time they are found to be offline. - pub OfflineSlash get(offline_slash) config(): Perbill = Perbill::from_millionths(1000); - /// Number of instances of offline reports before slashing begins for validators. - pub OfflineSlashGrace get(offline_slash_grace) config(): u32; /// Any validators that may never be slashed or forcibly kicked. It's a Vec since they're /// easy to initialize and the performance hit is minimal (we expect no more than four @@ -632,19 +613,20 @@ decl_storage! { config.stakers.iter().map(|&(_, _, value, _)| value).min().unwrap_or_default() }): BalanceOf; - /// 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): map T::AccountId => u32; - - /// Most recent `RECENT_OFFLINE_COUNT` instances. (Who it was, when it was reported, how - /// many instances they were offline for). - pub RecentlyOffline get(recently_offline): Vec<(T::AccountId, T::BlockNumber, u32)>; - /// True if the next session change will be a new era regardless of index. pub ForceEra get(force_era) config(): Forcing; + /// The percentage of the slash that is distributed to reporters. + /// + /// The rest of the slashed value is handled by the `Slash`. + pub SlashRewardFraction get(slash_reward_fraction) config(): Perbill; + /// A mapping from still-bonded eras to the first session index of that era. BondedEras: Vec<(EraIndex, SessionIndex)>; + + /// All slashes that have occurred in a given era. + EraSlashJournal get(era_slash_journal): + map EraIndex => Vec>>; } add_extra_genesis { config(stakers): @@ -688,11 +670,11 @@ decl_event!( pub enum Event where Balance = BalanceOf, ::AccountId { /// All validators have been rewarded by the given balance. Reward(Balance), - /// One validator (and its nominators) has been given an offline-warning (it is still - /// within its grace). The accrued number of slashes is recorded, too. - OfflineWarning(AccountId, u32), /// One validator (and its nominators) has been slashed by the given amount. - OfflineSlash(AccountId, Balance), + Slash(AccountId, Balance), + /// An old slashing report from a prior era was discarded because it could + /// not be processed. + OldSlashingReportDiscarded(SessionIndex), } ); @@ -895,10 +877,6 @@ decl_module! { let controller = ensure_signed(origin)?; let ledger = Self::ledger(&controller).ok_or("not a controller")?; let stash = &ledger.stash; - ensure!( - prefs.unstake_threshold <= MAX_UNSTAKE_THRESHOLD, - "unstake threshold too large" - ); >::remove(stash); >::insert(stash, prefs); } @@ -1027,13 +1005,6 @@ decl_module! { ForceEra::put(Forcing::ForceNew); } - /// Set the offline slash grace period. - #[weight = SimpleDispatchInfo::FixedOperational(10_000)] - fn set_offline_slash_grace(origin, #[compact] new: u32) { - ensure_root(origin)?; - OfflineSlashGrace::put(new); - } - /// Set the validators who cannot be slashed (if any). #[weight = SimpleDispatchInfo::FixedOperational(10_000)] fn set_invulnerables(origin, validators: Vec) { @@ -1070,20 +1041,42 @@ impl Module { >::insert(controller, ledger); } - /// Slash a given validator by a specific amount. Removes the slash from the validator's - /// balance by preference, and reduces the nominators' balance if needed. - fn slash_validator(stash: &T::AccountId, slash: BalanceOf) { - // The exposure (backing stake) information of the validator to be slashed. - let exposure = Self::stakers(stash); + /// Slash a given validator by a specific amount with given (historical) exposure. + /// + /// Removes the slash from the validator's balance by preference, + /// and reduces the nominators' balance if needed. + /// + /// Returns the resulting `NegativeImbalance` to allow distributing the slashed amount and + /// pushes an entry onto the slash journal. + fn slash_validator( + stash: &T::AccountId, + slash: BalanceOf, + exposure: &Exposure>, + journal: &mut Vec>>, + ) -> NegativeImbalanceOf { // The amount we are actually going to slash (can't be bigger than the validator's total // exposure) let slash = slash.min(exposure.total); + + // limit what we'll slash of the stash's own to only what's in + // the exposure. + // + // note: this is fine only because we limit reports of the current era. + // otherwise, these funds may have already been slashed due to something + // reported from a prior era. + let already_slashed_own = journal.iter() + .filter(|entry| &entry.who == stash) + .map(|entry| entry.own_slash) + .fold(>::zero(), |a, c| a.saturating_add(c)); + + let own_remaining = exposure.own.saturating_sub(already_slashed_own); + // The amount we'll slash from the validator's stash directly. - let own_slash = exposure.own.min(slash); + let own_slash = own_remaining.min(slash); let (mut imbalance, missing) = T::Currency::slash(stash, own_slash); let own_slash = own_slash - missing; - // The amount remaining that we can't slash from the validator, that must be taken from the - // nominators. + // The amount remaining that we can't slash from the validator, + // that must be taken from the nominators. let rest_slash = slash - own_slash; if !rest_slash.is_zero() { // The total to be slashed from the nominators. @@ -1096,7 +1089,19 @@ impl Module { } } } - T::Slash::on_unbalanced(imbalance); + + journal.push(SlashJournalEntry { + who: stash.clone(), + own_slash: own_slash.clone(), + amount: slash, + }); + + // trigger the event + Self::deposit_event( + RawEvent::Slash(stash.clone(), slash) + ); + + imbalance } /// Actually make a payment to a staker. This uses the currency's reward function @@ -1154,9 +1159,10 @@ impl Module { fn new_session(session_index: SessionIndex) -> Option<(Vec, Vec<(T::AccountId, Exposure>)>)> { + let era_length = session_index.checked_sub(Self::current_era_start_session_index()).unwrap_or(0); match ForceEra::get() { Forcing::ForceNew => ForceEra::kill(), - Forcing::NotForcing if session_index % T::SessionsPerEra::get() == 0 => (), + Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (), _ => return None, } let validators = T::SessionInterface::validators(); @@ -1210,6 +1216,10 @@ impl Module { // Increment current era. let current_era = CurrentEra::mutate(|s| { *s += 1; *s }); + + // prune journal for last era. + >::remove(current_era - 1); + CurrentEraStartSessionIndex::mutate(|v| { *v = start_session_index; }); @@ -1325,13 +1335,9 @@ impl Module { equalize::(&mut assignments_with_votes, &mut exposures, tolerance, iterations); } - // Clear Stakers and reduce their slash_count. + // Clear Stakers. for v in Self::current_elected().iter() { >::remove(v); - let slash_count = >::take(v); - if slash_count > 1 { - >::insert(v, slash_count - 1); - } } // Populate Stakers and figure out the minimum stake behind a slot. @@ -1371,68 +1377,10 @@ impl Module { >::remove(&controller); } >::remove(stash); - >::remove(stash); >::remove(stash); >::remove(stash); } - /// Call when a validator is determined to be offline. `count` is the - /// number of offenses the validator has committed. - /// - /// NOTE: This is called with the controller (not the stash) account id. - pub fn on_offline_validator(controller: T::AccountId, count: usize) { - if let Some(l) = Self::ledger(&controller) { - let stash = l.stash; - - // Early exit if validator is invulnerable. - if Self::invulnerables().contains(&stash) { - return - } - - let slash_count = Self::slash_count(&stash); - let new_slash_count = slash_count + count as u32; - >::insert(&stash, new_slash_count); - let grace = Self::offline_slash_grace(); - - if RECENT_OFFLINE_COUNT > 0 { - let item = (stash.clone(), >::block_number(), count as u32); - >::mutate(|v| if v.len() >= RECENT_OFFLINE_COUNT { - let index = v.iter() - .enumerate() - .min_by_key(|(_, (_, block, _))| block) - .expect("v is non-empty; qed") - .0; - v[index] = item; - } else { - v.push(item); - }); - } - - let prefs = Self::validators(&stash); - let unstake_threshold = prefs.unstake_threshold.min(MAX_UNSTAKE_THRESHOLD); - let max_slashes = grace + unstake_threshold; - - let event = if new_slash_count > max_slashes { - let slash_exposure = Self::stakers(&stash).total; - let offline_slash_base = Self::offline_slash() * slash_exposure; - // They're bailing. - let slash = offline_slash_base - // Multiply slash_mantissa by 2^(unstake_threshold with upper bound) - .checked_shl(unstake_threshold) - .map(|x| x.min(slash_exposure)) - .unwrap_or(slash_exposure); - let _ = Self::slash_validator(&stash, slash); - let _ = T::SessionInterface::disable_validator(&stash); - - RawEvent::OfflineSlash(stash.clone(), slash) - } else { - RawEvent::OfflineWarning(stash.clone(), slash_count) - }; - - Self::deposit_event(event); - } - } - /// Add reward points to validators using their stash account ID. /// /// Validators are keyed by stash account ID and must be in the current elected set. @@ -1564,3 +1512,108 @@ impl SelectInitialValidators for Module { >::select_validators().1 } } + +/// This is intended to be used with `FilterHistoricalOffences`. +impl OnOffenceHandler> for Module where + T: session::Trait::AccountId>, + T: session::historical::Trait< + FullIdentification = Exposure<::AccountId, BalanceOf>, + FullIdentificationOf = ExposureOf, + >, + T::SessionHandler: session::SessionHandler<::AccountId>, + T::OnSessionEnding: session::OnSessionEnding<::AccountId>, + T::SelectInitialValidators: session::SelectInitialValidators<::AccountId>, + T::ValidatorIdOf: Convert<::AccountId, Option<::AccountId>> +{ + fn on_offence( + offenders: &[OffenceDetails>], + slash_fraction: &[Perbill], + ) { + let mut remaining_imbalance = >::zero(); + let slash_reward_fraction = SlashRewardFraction::get(); + + let era_now = Self::current_era(); + let mut journal = Self::era_slash_journal(era_now); + for (details, slash_fraction) in offenders.iter().zip(slash_fraction) { + let stash = &details.offender.0; + let exposure = &details.offender.1; + + // Skip if the validator is invulnerable. + if Self::invulnerables().contains(stash) { + continue + } + + // calculate the amount to slash + let slash_exposure = exposure.total; + let amount = *slash_fraction * slash_exposure; + // in some cases `slash_fraction` can be just `0`, + // which means we are not slashing this time. + if amount.is_zero() { + continue; + } + + // make sure to disable validator in next sessions + let _ = T::SessionInterface::disable_validator(stash); + // force a new era, to select a new validator set + ForceEra::put(Forcing::ForceNew); + // actually slash the validator + let slashed_amount = Self::slash_validator(stash, amount, exposure, &mut journal); + + // distribute the rewards according to the slash + let slash_reward = slash_reward_fraction * slashed_amount.peek(); + if !slash_reward.is_zero() && !details.reporters.is_empty() { + let (mut reward, rest) = slashed_amount.split(slash_reward); + // split the reward between reporters equally. Division cannot fail because + // we guarded against it in the enclosing if. + let per_reporter = reward.peek() / (details.reporters.len() as u32).into(); + for reporter in &details.reporters { + let (reporter_reward, rest) = reward.split(per_reporter); + reward = rest; + T::Currency::resolve_creating(reporter, reporter_reward); + } + // The rest goes to the treasury. + remaining_imbalance.subsume(reward); + remaining_imbalance.subsume(rest); + } else { + remaining_imbalance.subsume(slashed_amount); + } + } + >::insert(era_now, journal); + + // Handle the rest of imbalances + T::Slash::on_unbalanced(remaining_imbalance); + } +} + +/// Filter historical offences out and only allow those from the current era. +pub struct FilterHistoricalOffences { + _inner: rstd::marker::PhantomData<(T, R)>, +} + +impl ReportOffence + for FilterHistoricalOffences, R> where + T: Trait, + R: ReportOffence, + O: Offence, +{ + fn report_offence(reporters: Vec, offence: O) { + // disallow any slashing from before the current era. + let offence_session = offence.session_index(); + if offence_session >= >::current_era_start_session_index() { + R::report_offence(reporters, offence) + } else { + >::deposit_event( + RawEvent::OldSlashingReportDiscarded(offence_session).into() + ) + } + } +} + +/// Returns the currently elected validator set represented by their stash accounts. +pub struct CurrentElectedStashAccounts(rstd::marker::PhantomData); + +impl CurrentElectedSet for CurrentElectedStashAccounts { + fn current_elected_set() -> Vec { + >::current_elected() + } +} diff --git a/srml/staking/src/mock.rs b/srml/staking/src/mock.rs index 19192144a9373..730c8306cb90c 100644 --- a/srml/staking/src/mock.rs +++ b/srml/staking/src/mock.rs @@ -20,6 +20,7 @@ use std::{collections::HashSet, cell::RefCell}; use sr_primitives::Perbill; use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize}; use sr_primitives::testing::{Header, UintAuthorityId}; +use sr_staking_primitives::SessionIndex; use primitives::{H256, Blake2Hasher}; use runtime_io; use srml_support::{assert_ok, impl_outer_origin, parameter_types, EnumerableStorageMap}; @@ -73,8 +74,8 @@ impl session::SessionHandler for TestSessionHandler { } } -pub fn is_disabled(validator: AccountId) -> bool { - let stash = Staking::ledger(&validator).unwrap().stash; +pub fn is_disabled(controller: AccountId) -> bool { + let stash = Staking::ledger(&controller).unwrap().stash; SESSION.with(|d| d.borrow().1.contains(&stash)) } @@ -181,7 +182,7 @@ impl timestamp::Trait for Test { type MinimumPeriod = MinimumPeriod; } parameter_types! { - pub const SessionsPerEra: session::SessionIndex = 3; + pub const SessionsPerEra: SessionIndex = 3; pub const BondingDuration: EraIndex = 3; } impl Trait for Test { @@ -205,6 +206,7 @@ pub struct ExtBuilder { minimum_validator_count: u32, fair: bool, num_validators: Option, + invulnerables: Vec, } impl Default for ExtBuilder { @@ -217,6 +219,7 @@ impl Default for ExtBuilder { minimum_validator_count: 0, fair: true, num_validators: None, + invulnerables: vec![], } } } @@ -250,6 +253,10 @@ impl ExtBuilder { self.num_validators = Some(num_validators); self } + pub fn invulnerables(mut self, invulnerables: Vec) -> Self { + self.invulnerables = invulnerables; + self + } pub fn set_associated_consts(&self) { EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); } @@ -300,6 +307,7 @@ impl ExtBuilder { let _ = GenesisConfig::{ current_era: 0, stakers: vec![ + // (stash, controller, staked_amount, status) (11, 10, balance_factor * 1000, StakerStatus::::Validator), (21, 20, stake_21, StakerStatus::::Validator), (31, 30, stake_31, StakerStatus::::Validator), @@ -309,10 +317,9 @@ impl ExtBuilder { ], validator_count: self.validator_count, minimum_validator_count: self.minimum_validator_count, - offline_slash: Perbill::from_percent(5), - offline_slash_grace: 0, - invulnerables: vec![], - .. Default::default() + invulnerables: self.invulnerables, + slash_reward_fraction: Perbill::from_percent(10), + ..Default::default() }.assimilate_storage(&mut storage); let _ = session::GenesisConfig:: { @@ -398,7 +405,12 @@ pub fn bond_nominator(acc: u64, val: u64, target: Vec) { assert_ok!(Staking::nominate(Origin::signed(acc), target)); } -pub fn start_session(session_index: session::SessionIndex) { +pub fn advance_session() { + let current_index = Session::current_index(); + start_session(current_index + 1); +} + +pub fn start_session(session_index: SessionIndex) { // Compensate for session delay let session_index = session_index + 1; for i in Session::current_index()..session_index { diff --git a/srml/staking/src/tests.rs b/srml/staking/src/tests.rs index de473dd9902fc..55cebd3eb0766 100644 --- a/srml/staking/src/tests.rs +++ b/srml/staking/src/tests.rs @@ -20,6 +20,7 @@ use super::*; use runtime_io::with_externalities; use phragmen; use sr_primitives::traits::OnInitialize; +use sr_staking_primitives::offence::{OffenceDetails, OnOffenceHandler}; use srml_support::{assert_ok, assert_noop, assert_eq_uvec, EnumerableStorageMap}; use mock::*; use srml_support::traits::{Currency, ReservableCurrency}; @@ -41,11 +42,11 @@ fn basic_setup_works() { // Account 1 does not control any stash assert_eq!(Staking::ledger(&1), None); - // ValidatorPrefs are default, thus unstake_threshold is 3, other values are default for their type + // ValidatorPrefs are default assert_eq!(>::enumerate().collect::>(), vec![ - (31, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 }), - (21, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 }), - (11, ValidatorPrefs { unstake_threshold: 3, validator_payment: 0 }) + (31, ValidatorPrefs::default()), + (21, ValidatorPrefs::default()), + (11, ValidatorPrefs::default()) ]); // Account 100 is the default nominator @@ -83,32 +84,16 @@ fn basic_setup_works() { // Initial Era and session assert_eq!(Staking::current_era(), 0); - // initial slash_count of validators - assert_eq!(Staking::slash_count(&11), 0); - assert_eq!(Staking::slash_count(&21), 0); - - // All exposures must be correct. - check_exposure_all(); - check_nominator_all(); - }); -} - -#[test] -fn no_offline_should_work() { - // Test the staking module works when no validators are offline - with_externalities(&mut ExtBuilder::default().build(), - || { - // Slashing begins for validators immediately if found offline - assert_eq!(Staking::offline_slash_grace(), 0); - // Account 10 has not been reported offline - assert_eq!(Staking::slash_count(&10), 0); // Account 10 has `balance_factor` free balance assert_eq!(Balances::free_balance(&10), 1); - // Nothing happens to Account 10, as expected - assert_eq!(Staking::slash_count(&10), 0); assert_eq!(Balances::free_balance(&10), 1); + // New era is not being forced assert_eq!(Staking::force_era(), Forcing::NotForcing); + + // All exposures must be correct. + check_exposure_all(); + check_nominator_all(); }); } @@ -135,183 +120,6 @@ fn change_controller_works() { }) } -#[test] -fn invulnerability_should_work() { - // Test that users can be invulnerable from slashing and being kicked - with_externalities(&mut ExtBuilder::default().build(), - || { - // Make account 11 invulnerable - assert_ok!(Staking::set_invulnerables(Origin::ROOT, vec![11])); - // Give account 11 some funds - let _ = Balances::make_free_balance_be(&11, 70); - // There is no slash grace -- slash immediately. - assert_eq!(Staking::offline_slash_grace(), 0); - // Account 11 has not been slashed - assert_eq!(Staking::slash_count(&11), 0); - // Account 11 has the 70 funds we gave it above - assert_eq!(Balances::free_balance(&11), 70); - // Account 11 should be a validator - assert!(>::exists(&11)); - - // Set account 11 as an offline validator with a large number of reports - // Should exit early if invulnerable - Staking::on_offline_validator(10, 100); - - // Show that account 11 has not been touched - assert_eq!(Staking::slash_count(&11), 0); - assert_eq!(Balances::free_balance(&11), 70); - assert!(>::exists(&11)); - // New era not being forced - // NOTE: new era is always forced once slashing happens -> new validators need to be chosen. - assert_eq!(Staking::force_era(), Forcing::NotForcing); - }); -} - -#[test] -fn offline_should_slash_and_disable() { - // Test that an offline validator gets slashed and kicked - with_externalities(&mut ExtBuilder::default().build(), || { - // Give account 10 some balance - let _ = Balances::make_free_balance_be(&11, 1000); - // Confirm account 10 is a validator - assert!(>::exists(&11)); - // Validators get slashed immediately - assert_eq!(Staking::offline_slash_grace(), 0); - // Unstake threshold is 3 - assert_eq!(Staking::validators(&11).unstake_threshold, 3); - // Account 10 has not been slashed before - assert_eq!(Staking::slash_count(&11), 0); - // Account 10 has the funds we just gave it - assert_eq!(Balances::free_balance(&11), 1000); - // Account 10 is not yet disabled. - assert!(!is_disabled(10)); - // Report account 10 as offline, one greater than unstake threshold - Staking::on_offline_validator(10, 4); - // Confirm user has been reported - assert_eq!(Staking::slash_count(&11), 4); - // Confirm balance has been reduced by 2^unstake_threshold * offline_slash() * amount_at_stake. - let slash_base = Staking::offline_slash() * Staking::stakers(11).total; - assert_eq!(Balances::free_balance(&11), 1000 - 2_u64.pow(3) * slash_base); - // Confirm account 10 has been disabled. - assert!(is_disabled(10)); - }); -} - -#[test] -fn offline_grace_should_delay_slashing() { - // Tests that with grace, slashing is delayed - with_externalities(&mut ExtBuilder::default().build(), || { - // Initialize account 10 with balance - let _ = Balances::make_free_balance_be(&11, 70); - // Verify account 11 has balance - assert_eq!(Balances::free_balance(&11), 70); - - // Set offline slash grace - let offline_slash_grace = 1; - assert_ok!(Staking::set_offline_slash_grace(Origin::ROOT, offline_slash_grace)); - assert_eq!(Staking::offline_slash_grace(), 1); - - // Check unstake_threshold is 3 (default) - let default_unstake_threshold = 3; - assert_eq!( - Staking::validators(&11), - ValidatorPrefs { unstake_threshold: default_unstake_threshold, validator_payment: 0 } - ); - - // Check slash count is zero - assert_eq!(Staking::slash_count(&11), 0); - - // Report account 10 up to the threshold - Staking::on_offline_validator(10, default_unstake_threshold as usize + offline_slash_grace as usize); - // Confirm slash count - assert_eq!(Staking::slash_count(&11), 4); - - // Nothing should happen - assert_eq!(Balances::free_balance(&11), 70); - - // Report account 10 one more time - Staking::on_offline_validator(10, 1); - assert_eq!(Staking::slash_count(&11), 5); - // User gets slashed - assert!(Balances::free_balance(&11) < 70); - // New era is forced - assert!(is_disabled(10)); - }); -} - - -#[test] -fn max_unstake_threshold_works() { - // Tests that max_unstake_threshold gets used when prefs.unstake_threshold is large - with_externalities(&mut ExtBuilder::default().build(), || { - const MAX_UNSTAKE_THRESHOLD: u32 = 10; - // Two users with maximum possible balance - let _ = Balances::make_free_balance_be(&11, u64::max_value()); - let _ = Balances::make_free_balance_be(&21, u64::max_value()); - - // Give them full exposure as a staker - >::insert(&11, Exposure { total: 1000000, own: 1000000, others: vec![]}); - >::insert(&21, Exposure { total: 2000000, own: 2000000, others: vec![]}); - - // Check things are initialized correctly - assert_eq!(Balances::free_balance(&11), u64::max_value()); - assert_eq!(Balances::free_balance(&21), u64::max_value()); - assert_eq!(Staking::offline_slash_grace(), 0); - // Account 10 will have max unstake_threshold - assert_ok!(Staking::validate(Origin::signed(10), ValidatorPrefs { - unstake_threshold: MAX_UNSTAKE_THRESHOLD, - validator_payment: 0, - })); - // Account 20 could not set their unstake_threshold past 10 - assert_noop!(Staking::validate(Origin::signed(20), ValidatorPrefs { - unstake_threshold: MAX_UNSTAKE_THRESHOLD + 1, - validator_payment: 0}), - "unstake threshold too large" - ); - // Give Account 20 unstake_threshold 11 anyway, should still be limited to 10 - >::insert(21, ValidatorPrefs { - unstake_threshold: MAX_UNSTAKE_THRESHOLD + 1, - validator_payment: 0, - }); - - OfflineSlash::put(Perbill::from_fraction(0.0001)); - - // Report each user 1 more than the max_unstake_threshold - Staking::on_offline_validator(10, MAX_UNSTAKE_THRESHOLD as usize + 1); - Staking::on_offline_validator(20, MAX_UNSTAKE_THRESHOLD as usize + 1); - - // Show that each balance only gets reduced by 2^max_unstake_threshold times 10% - // of their total stake. - assert_eq!(Balances::free_balance(&11), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 100); - assert_eq!(Balances::free_balance(&21), u64::max_value() - 2_u64.pow(MAX_UNSTAKE_THRESHOLD) * 200); - }); -} - -#[test] -fn slashing_does_not_cause_underflow() { - // Tests that slashing more than a user has does not underflow - with_externalities(&mut ExtBuilder::default().build(), || { - // Verify initial conditions - assert_eq!(Balances::free_balance(&11), 1000); - assert_eq!(Staking::offline_slash_grace(), 0); - - // Set validator preference so that 2^unstake_threshold would cause overflow (greater than 64) - // FIXME: that doesn't overflow. - >::insert(11, ValidatorPrefs { - unstake_threshold: 10, - validator_payment: 0, - }); - - System::set_block_number(1); - Session::on_initialize(System::block_number()); - - // Should not panic - Staking::on_offline_validator(10, 100); - // Confirm that underflow has not occurred, and account balance is set to zero - assert_eq!(Balances::free_balance(&11), 0); - }); -} - #[test] fn rewards_should_work() { // should check that: @@ -748,13 +556,12 @@ fn nominating_and_rewards_should_work() { #[test] fn nominators_also_get_slashed() { // A nominator should be slashed if the validator they nominated is slashed + // Here is the breakdown of roles: + // 10 - is the controller of 11 + // 11 - is the stash. + // 2 - is the nominator of 20, 10 with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { assert_eq!(Staking::validator_count(), 2); - // slash happens immediately. - assert_eq!(Staking::offline_slash_grace(), 0); - // Account 10 has not been reported offline - assert_eq!(Staking::slash_count(&10), 0); - OfflineSlash::put(Perbill::from_percent(12)); // Set payee to controller assert_ok!(Staking::set_payee(Origin::signed(10), RewardDestination::Controller)); @@ -765,7 +572,7 @@ fn nominators_also_get_slashed() { let _ = Balances::make_free_balance_be(i, initial_balance); } - // 2 will nominate for 10 + // 2 will nominate for 10, 20 let nominator_stake = 500; assert_ok!(Staking::bond(Origin::signed(1), 2, nominator_stake, RewardDestination::default())); assert_ok!(Staking::nominate(Origin::signed(2), vec![20, 10])); @@ -781,15 +588,24 @@ fn nominators_also_get_slashed() { assert_eq!(Balances::total_balance(&2), initial_balance); // 10 goes offline - Staking::on_offline_validator(10, 4); - let expo = Staking::stakers(10); - let slash_value = Staking::offline_slash() * expo.total * 2_u64.pow(3); + Staking::on_offence( + &[OffenceDetails { + offender: ( + 11, + Staking::stakers(&11), + ), + reporters: vec![], + }], + &[Perbill::from_percent(5)], + ); + let expo = Staking::stakers(11); + let slash_value = 50; let total_slash = expo.total.min(slash_value); let validator_slash = expo.own.min(total_slash); let nominator_slash = nominator_stake.min(total_slash - validator_slash); // initial + first era reward + slash - assert_eq!(Balances::total_balance(&10), initial_balance + total_payout - validator_slash); + assert_eq!(Balances::total_balance(&11), initial_balance - validator_slash); assert_eq!(Balances::total_balance(&2), initial_balance - nominator_slash); check_exposure_all(); check_nominator_all(); @@ -907,10 +723,11 @@ fn forcing_new_era_works() { start_session(6); assert_eq!(Staking::current_era(), 1); - // back to normal + // back to normal. + // this immediatelly starts a new session. ForceEra::put(Forcing::NotForcing); start_session(7); - assert_eq!(Staking::current_era(), 1); + assert_eq!(Staking::current_era(), 2); start_session(8); assert_eq!(Staking::current_era(), 2); @@ -1100,7 +917,6 @@ fn validator_payment_prefs_work() { }); >::insert(&2, RewardDestination::Stash); >::insert(&11, ValidatorPrefs { - unstake_threshold: 3, validator_payment: validator_cut }); @@ -1337,13 +1153,6 @@ fn slot_stake_is_least_staked_validator_and_exposure_defines_maximum_punishment( // -- slot stake should also be updated. assert_eq!(Staking::slot_stake(), 69 + total_payout_0/2); - // If 10 gets slashed now, it will be slashed by 5% of exposure.total * 2.pow(unstake_thresh) - Staking::on_offline_validator(10, 4); - // Confirm user has been reported - assert_eq!(Staking::slash_count(&11), 4); - // check the balance of 10 (slash will be deducted from free balance.) - assert_eq!(Balances::free_balance(&11), _11_balance - _11_balance*5/100 * 2u64.pow(3)); - check_exposure_all(); check_nominator_all(); }); @@ -1365,8 +1174,6 @@ fn on_free_balance_zero_stash_removes_validator() { assert_eq!(Staking::bonded(&11), Some(10)); // Set some storage items which we expect to be cleaned up - // Initiate slash count storage item - Staking::on_offline_validator(10, 1); // Set payee information assert_ok!(Staking::set_payee(Origin::signed(10), RewardDestination::Stash)); @@ -1374,7 +1181,6 @@ fn on_free_balance_zero_stash_removes_validator() { assert!(>::exists(&10)); assert!(>::exists(&11)); assert!(>::exists(&11)); - assert!(>::exists(&11)); assert!(>::exists(&11)); // Reduce free_balance of controller to 0 @@ -1389,7 +1195,6 @@ fn on_free_balance_zero_stash_removes_validator() { assert!(>::exists(&10)); assert!(>::exists(&11)); assert!(>::exists(&11)); - assert!(>::exists(&11)); assert!(>::exists(&11)); // Reduce free_balance of stash to 0 @@ -1402,7 +1207,6 @@ fn on_free_balance_zero_stash_removes_validator() { assert!(!>::exists(&11)); assert!(!>::exists(&11)); assert!(!>::exists(&11)); - assert!(!>::exists(&11)); assert!(!>::exists(&11)); }); } @@ -1459,7 +1263,6 @@ fn on_free_balance_zero_stash_removes_nominator() { assert!(!>::exists(&11)); assert!(!>::exists(&11)); assert!(!>::exists(&11)); - assert!(!>::exists(&11)); assert!(!>::exists(&11)); }); } @@ -2107,7 +1910,7 @@ fn reward_validator_slashing_validator_doesnt_overflow() { ]}); // Check slashing - Staking::slash_validator(&11, reward_slash); + let _ = Staking::slash_validator(&11, reward_slash, &Staking::stakers(&11), &mut Vec::new()); assert_eq!(Balances::total_balance(&11), stake - 1); assert_eq!(Balances::total_balance(&2), 1); }) @@ -2180,3 +1983,150 @@ fn unbonded_balance_is_not_slashable() { assert_eq!(Staking::slashable_balance_of(&11), 200); }) } + +#[test] +fn era_is_always_same_length() { + // This ensures that the sessions is always of the same length if there is no forcing no + // session changes. + with_externalities(&mut ExtBuilder::default().build(), || { + start_era(1); + assert_eq!(Staking::current_era_start_session_index(), SessionsPerEra::get()); + + start_era(2); + assert_eq!(Staking::current_era_start_session_index(), SessionsPerEra::get() * 2); + + let session = Session::current_index(); + ForceEra::put(Forcing::ForceNew); + advance_session(); + assert_eq!(Staking::current_era(), 3); + assert_eq!(Staking::current_era_start_session_index(), session + 1); + + start_era(4); + assert_eq!(Staking::current_era_start_session_index(), session + SessionsPerEra::get() + 1); + }); +} + +#[test] +fn offence_forces_new_era() { + with_externalities(&mut ExtBuilder::default().build(), || { + Staking::on_offence( + &[OffenceDetails { + offender: ( + 11, + Staking::stakers(&11), + ), + reporters: vec![], + }], + &[Perbill::from_percent(5)], + ); + + assert_eq!(Staking::force_era(), Forcing::ForceNew); + }); +} + +#[test] +fn slashing_performed_according_exposure() { + // This test checks that slashing is performed according the exposure (or more precisely, + // historical exposure), not the current balance. + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Staking::stakers(&11).own, 1000); + + // Handle an offence with a historical exposure. + Staking::on_offence( + &[OffenceDetails { + offender: ( + 11, + Exposure { + total: 500, + own: 500, + others: vec![], + }, + ), + reporters: vec![], + }], + &[Perbill::from_percent(50)], + ); + + // The stash account should be slashed for 250 (50% of 500). + assert_eq!(Balances::free_balance(&11), 1000 - 250); + }); +} + +#[test] +fn reporters_receive_their_slice() { + // This test verifies that the reporters of the offence receive their slice from the slashed + // amount. + with_externalities(&mut ExtBuilder::default().build(), || { + // The reporters' reward is calculated from the total exposure. + assert_eq!(Staking::stakers(&11).total, 1250); + + Staking::on_offence( + &[OffenceDetails { + offender: ( + 11, + Staking::stakers(&11), + ), + reporters: vec![1, 2], + }], + &[Perbill::from_percent(50)], + ); + + // 1250 x 50% (slash fraction) x 10% (rewards slice) + assert_eq!(Balances::free_balance(&1), 10 + 31); + assert_eq!(Balances::free_balance(&2), 20 + 31); + }); +} + +#[test] +fn invulnerables_are_not_slashed() { + // For invulnerable validators no slashing is performed. + with_externalities( + &mut ExtBuilder::default().invulnerables(vec![11]).build(), + || { + assert_eq!(Balances::free_balance(&11), 1000); + assert_eq!(Balances::free_balance(&21), 2000); + assert_eq!(Staking::stakers(&21).total, 1250); + + Staking::on_offence( + &[ + OffenceDetails { + offender: (11, Staking::stakers(&11)), + reporters: vec![], + }, + OffenceDetails { + offender: (21, Staking::stakers(&21)), + reporters: vec![], + }, + ], + &[Perbill::from_percent(50), Perbill::from_percent(20)], + ); + + // The validator 11 hasn't been slashed, but 21 has been. + assert_eq!(Balances::free_balance(&11), 1000); + assert_eq!(Balances::free_balance(&21), 1750); // 2000 - (0.2 * 1250) + }, + ); +} + +#[test] +fn dont_slash_if_fraction_is_zero() { + // Don't slash if the fraction is zero. + with_externalities(&mut ExtBuilder::default().build(), || { + assert_eq!(Balances::free_balance(&11), 1000); + + Staking::on_offence( + &[OffenceDetails { + offender: ( + 11, + Staking::stakers(&11), + ), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + ); + + // The validator hasn't been slashed. The new era is not forced. + assert_eq!(Balances::free_balance(&11), 1000); + assert_eq!(Staking::force_era(), Forcing::NotForcing); + }); +} diff --git a/srml/support/src/traits.rs b/srml/support/src/traits.rs index a7d8c9e6ba0fa..ff667879af740 100644 --- a/srml/support/src/traits.rs +++ b/srml/support/src/traits.rs @@ -117,8 +117,8 @@ pub trait VerifySeal { pub trait KeyOwnerProofSystem { /// The proof of membership itself. type Proof: Codec; - /// The full identification of a key owner. - type FullIdentification: Codec; + /// The full identification of a key owner and the stash account. + type IdentificationTuple: Codec; /// Prove membership of a key owner in the current block-state. /// @@ -131,7 +131,7 @@ pub trait KeyOwnerProofSystem { /// Check a proof of membership on-chain. Return `Some` iff the proof is /// valid and recent enough to check. - fn check_proof(key: Key, proof: Self::Proof) -> Option; + fn check_proof(key: Key, proof: Self::Proof) -> Option; } /// Handler for when some currency "account" decreased in balance for