diff --git a/Cargo.lock b/Cargo.lock index 8810fc7ebb98f..9e6df9e820fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2361,6 +2361,7 @@ dependencies = [ "srml-grandpa 2.0.0", "srml-im-online 0.1.0", "srml-indices 2.0.0", + "srml-rolling-window 2.0.0", "srml-session 2.0.0", "srml-staking 2.0.0", "srml-sudo 2.0.0", @@ -3918,6 +3919,24 @@ dependencies = [ "substrate-primitives 2.0.0", ] +[[package]] +name = "srml-rolling-window" +version = "2.0.0" +dependencies = [ + "parity-codec 4.1.3 (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-std 2.0.0", + "srml-balances 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", + "substrate-primitives 2.0.0", +] + [[package]] name = "srml-session" version = "2.0.0" @@ -3949,6 +3968,7 @@ dependencies = [ "sr-std 2.0.0", "srml-authorship 0.1.0", "srml-balances 2.0.0", + "srml-rolling-window 2.0.0", "srml-session 2.0.0", "srml-support 2.0.0", "srml-system 2.0.0", diff --git a/Cargo.toml b/Cargo.toml index a6a7b8d17ba35..e0b95f8103b9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ members = [ "srml/staking", "srml/sudo", "srml/system", + "srml/rolling-window", "srml/timestamp", "srml/treasury", "node/cli", diff --git a/node/runtime/Cargo.toml b/node/runtime/Cargo.toml index c40e6d779644c..fd87ec32d08c8 100644 --- a/node/runtime/Cargo.toml +++ b/node/runtime/Cargo.toml @@ -29,6 +29,7 @@ executive = { package = "srml-executive", path = "../../srml/executive", default finality-tracker = { package = "srml-finality-tracker", path = "../../srml/finality-tracker", default-features = false } grandpa = { package = "srml-grandpa", path = "../../srml/grandpa", default-features = false } indices = { package = "srml-indices", path = "../../srml/indices", default-features = false } +rolling-window = { package = "srml-rolling-window", path = "../../srml/rolling-window", default-features = false } session = { package = "srml-session", path = "../../srml/session", default-features = false, features = ["historical"] } staking = { package = "srml-staking", path = "../../srml/staking", default-features = false } system = { package = "srml-system", path = "../../srml/system", default-features = false } @@ -80,6 +81,7 @@ std = [ "safe-mix/std", "client/std", "rustc-hex", + "rolling-window/std", "substrate-keyring", "offchain-primitives/std", "im-online/std", diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 9ab7b84853e89..4ee6808ec657e 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -187,7 +187,7 @@ impl authorship::Trait for Runtime { type EventHandler = Staking; } -type SessionHandlers = (Grandpa, Babe, ImOnline); +type SessionHandlers = (Grandpa, Babe, ImOnline, RollingWindow); impl_opaque_keys! { pub struct SessionKeys { @@ -238,6 +238,10 @@ impl staking::Trait for Runtime { type SessionInterface = Self; } +impl staking::slash::Trait for Runtime { + type Currency = Balances; +} + parameter_types! { pub const LaunchPeriod: BlockNumber = 28 * 24 * 60 * MINUTES; pub const VotingPeriod: BlockNumber = 28 * 24 * 60 * MINUTES; @@ -377,6 +381,10 @@ impl im_online::Trait for Runtime { type IsValidAuthorityId = Babe; } +impl rolling_window::Trait for Runtime { + type SessionKey = primitives::sr25519::Public; +} + impl grandpa::Trait for Runtime { type Event = Event; } @@ -416,6 +424,7 @@ construct_runtime!( Contracts: contracts, Sudo: sudo, ImOnline: im_online::{default, ValidateUnsigned}, + RollingWindow: rolling_window::{Module, Storage}, } ); diff --git a/srml/aura/src/lib.rs b/srml/aura/src/lib.rs index 1402cb1d9e472..4023c064e30b8 100644 --- a/srml/aura/src/lib.rs +++ b/srml/aura/src/lib.rs @@ -66,6 +66,7 @@ use inherents::{InherentDataProviders, ProvideInherentData}; use substrate_consensus_aura_primitives::{AURA_ENGINE_ID, ConsensusLog}; #[cfg(feature = "std")] use parity_codec::Decode; +use session::SessionIndex; mod mock; mod tests; @@ -188,7 +189,7 @@ impl Module { impl session::OneSessionHandler for Module { type Key = T::AuthorityId; - fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I) + fn on_new_session<'a, I: 'a>(_: (SessionIndex, SessionIndex), changed: bool, validators: I, _queued_validators: I) where I: Iterator { // instant changes diff --git a/srml/babe/src/lib.rs b/srml/babe/src/lib.rs index a47fb04c2f4e7..6ed4e8d251dc5 100644 --- a/srml/babe/src/lib.rs +++ b/srml/babe/src/lib.rs @@ -34,6 +34,7 @@ use inherents::{RuntimeString, InherentIdentifier, InherentData, ProvideInherent use inherents::{InherentDataProviders, ProvideInherentData}; use babe_primitives::{BABE_ENGINE_ID, ConsensusLog, BabeWeight, Epoch, RawBabePreDigest}; pub use babe_primitives::{AuthorityId, VRF_OUTPUT_LENGTH, PUBLIC_KEY_LENGTH}; +use session::SessionIndex; /// The BABE inherent identifier. pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"babeslot"; @@ -273,8 +274,14 @@ impl OnTimestampSet for Module { impl session::OneSessionHandler for Module { type Key = AuthorityId; - fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, queued_validators: I) - where I: Iterator + + fn on_new_session<'a, I: 'a>( + _: (SessionIndex, SessionIndex), + _changed: bool, + validators: I, + queued_validators: I, + ) where + I: Iterator { use staking::BalanceOf; let to_votes = |b: BalanceOf| { diff --git a/srml/grandpa/src/lib.rs b/srml/grandpa/src/lib.rs index 6f1c897e53ba5..3f01f8edd7c7f 100644 --- a/srml/grandpa/src/lib.rs +++ b/srml/grandpa/src/lib.rs @@ -41,6 +41,7 @@ use sr_primitives::{ use fg_primitives::{ScheduledChange, ConsensusLog, GRANDPA_ENGINE_ID}; pub use fg_primitives::{AuthorityId, AuthorityWeight}; use system::{ensure_signed, DigestOf}; +use session::SessionIndex; mod mock; mod tests; @@ -346,7 +347,7 @@ impl Module { impl session::OneSessionHandler for Module { type Key = AuthorityId; - fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I) + fn on_new_session<'a, I: 'a>(_: (SessionIndex, SessionIndex), changed: bool, validators: I, _queued_validators: I) where I: Iterator { // instant changes diff --git a/srml/im-online/src/lib.rs b/srml/im-online/src/lib.rs index 8254cb60780fd..009db74fd0da1 100644 --- a/srml/im-online/src/lib.rs +++ b/srml/im-online/src/lib.rs @@ -391,7 +391,7 @@ impl Module { impl session::OneSessionHandler for Module { type Key = ::AuthorityId; - fn on_new_session<'a, I: 'a>(_changed: bool, _validators: I, _next_validators: I) { + fn on_new_session<'a, I: 'a>(_: (SessionIndex, SessionIndex), _changed: bool, _validators: I, _next_validators: I) { Self::new_session(); } diff --git a/srml/rolling-window/Cargo.toml b/srml/rolling-window/Cargo.toml new file mode 100644 index 0000000000000..905d2c7e7251c --- /dev/null +++ b/srml/rolling-window/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "srml-rolling-window" +version = "2.0.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +balances = { package = "srml-balances", path = "../balances", default-features = false } +parity-codec = { version = "4.1.1", default-features = false } +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 } +srml-support = { path = "../support", default-features = false } +srml-session = { path = "../session", default-features = false } +system = { package = "srml-system", path = "../system", default-features = false } + +[dev-dependencies] +balances = { package = "srml-balances", path = "../balances" } +runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false } +substrate-primitives = { path = "../../core/primitives" } +timestamp = { package = "srml-timestamp", path = "../timestamp" } +srml-staking = { path = "../staking" } + +[features] +default = ["std"] +std = [ + "balances/std", + "parity-codec/std", + "rstd/std", + "serde", + "sr-primitives/std", + "srml-session/std", + "srml-support/std", + "system/std", +] diff --git a/srml/rolling-window/src/lib.rs b/srml/rolling-window/src/lib.rs new file mode 100644 index 0000000000000..e6a61c53ad948 --- /dev/null +++ b/srml/rolling-window/src/lib.rs @@ -0,0 +1,296 @@ +// 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 . + +//! # Rolling Window module +//! +//! ## Overview +//! +//! The Rolling Window Module is similar to `simple moving average` except +//! that it just reports the number of occurrences in the window instead of +//! calculating the average. +//! +//! It is mainly implemented to keep track of misbehaviors and only to take +//! the last `sessions` of misbehaviors into account. +//! + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs, rust_2018_idioms)] + +#[cfg(test)] +mod mock; + +use srml_support::{ + StorageMap, Parameter, EnumerableStorageMap, decl_module, decl_storage, + traits::SlashingOffence, +}; +use parity_codec::{Codec, Decode, Encode}; +use rstd::vec::Vec; +use sr_primitives::traits::{Member, MaybeSerializeDebug, TypedKey}; +use srml_session::{SessionIndex, OneSessionHandler}; + +/// Rolling Window trait +pub trait Trait: system::Trait { + /// Identifier type to implement `OneSessionHandler` + type SessionKey: Member + Parameter + Default + TypedKey + Decode + Encode + AsRef<[u8]>; +} + +type Id = [u8; 16]; +type WindowLength = u32; + +decl_storage! { + trait Store for Module as RollingWindow { + /// Misbehavior reports + /// + /// It stores every unique misbehavior of a kind + // TODO [#3149]: optimize how to shrink the window when sessions expire + MisbehaviorReports get(misbehavior_reports): linked_map Id => Vec<(SessionIndex, WindowLength)>; + + /// Bonding Uniqueness + /// + /// Keeps track of uniquely reported misconducts in the entire bonding duration + /// which is currently unbounded (insert only) + /// + /// Footprints need to be unique or stash accounts must be banned from joining + /// the validator set after been slashed + BondingUniqueness get(bonding_uniqueness): linked_map T::Hash => SessionIndex; + } +} + +decl_module! { + /// Rolling Window module + pub struct Module for enum Call where origin: T::Origin {} +} + +/// Trait for getting the number of misbehaviors in the current window +pub trait GetMisbehaviors { + /// Get number of misbehavior's in the current window for a kind + fn get_misbehaviors(id: Id) -> u64; +} + +impl GetMisbehaviors for Module { + fn get_misbehaviors(id: Id) -> u64 { + MisbehaviorReports::get(id).len() as u64 + } +} + +/// Trait for reporting misbehavior's +pub trait MisbehaviorReporter { + /// Report misbehavior for a kind + fn report_misbehavior( + id: Id, + window_length: WindowLength, + footprint: Hash, + current_session: SessionIndex + ) -> Result<(), ()>; +} + +impl MisbehaviorReporter for Module { + fn report_misbehavior( + id: Id, + window_length: WindowLength, + footprint: T::Hash, + current_session: SessionIndex, + ) -> Result<(), ()> { + if >::exists(footprint) { + Err(()) + } else { + >::insert(footprint, current_session); + MisbehaviorReports::mutate(id, |entry| entry.push((current_session, window_length))); + Ok(()) + } + } +} + +impl OneSessionHandler for Module +{ + type Key = T::SessionKey; + + fn on_new_session<'a, I: 'a>( + (ended_session, _new_session): (SessionIndex, SessionIndex), + _changed: bool, + _validators: I, + _queued_validators: I, + ) { + for (kind, _) in MisbehaviorReports::enumerate() { + MisbehaviorReports::mutate(kind, |reports| { + // it is guaranteed that `reported_session` happened in the same session or before `ending` + reports.retain(|(reported_session, window_length)| { + let diff = ended_session.wrapping_sub(*reported_session); + diff < *window_length + }); + }); + } + } + + // ignored + fn on_disabled(_: usize) {} +} + +/// Macro for implement static `base_severity` which may be used for misconducts implementations +#[macro_export] +macro_rules! impl_base_severity { + // type with type parameters + ($ty:ident < $( $N:ident $(: $b0:ident $(+$b:ident)* )? ),* >, $t: ty : $seve: expr) => { + impl< $( $N $(: $b0 $(+$b)* )? ),* > $ty< $( $N ),* > { + fn base_severity() -> $t { + $seve + } + } + }; + // type without type parameters + ($ty:ident, $t: ty : $seve: expr) => { + impl $ty { + fn base_severity() -> $t { + $seve + } + } + }; +} + +/// Macro for implement static `misconduct kind` which may be used for misconducts implementations +/// Note, that the kind need to implement the `WindowLength` trait to work +#[macro_export] +macro_rules! impl_kind { + // type with type parameters + ($ty:ident < $( $N:ident $(: $b0:ident $(+$b:ident)* )? ),* >, $t: ty : $kind: expr) => { + + impl< $( $N $(: $b0 $(+$b)* )? ),* > $ty< $( $N ),* > { + fn kind() -> $t { + $kind + } + } + }; + // type without type parameters + ($ty:ident, $t: ty : $kind: expr) => { + impl $ty { + fn kind() -> $t { + $kind + } + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + use std::iter::empty; + use runtime_io::with_externalities; + use crate::mock::*; + use substrate_primitives::H256; + use srml_support::{assert_ok, assert_noop}; + + type RollingWindow = Module; + + #[test] + fn it_works() { + with_externalities(&mut new_test_ext(), || { + let zero = H256::zero(); + let one: H256 = [1_u8; 32].into(); + let two: H256 = [2_u8; 32].into(); + + let mut current_session = 0; + + assert_ok!(RollingWindow::report_misbehavior(Kind::Two, zero, current_session)); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 1); + assert_noop!(RollingWindow::report_misbehavior(Kind::Two, zero, current_session), ()); + + assert_ok!(RollingWindow::report_misbehavior(Kind::Two, one, current_session)); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 2); + assert_noop!(RollingWindow::report_misbehavior(Kind::Two, one, current_session), ()); + + current_session += 1; + RollingWindow::on_new_session((current_session - 1, current_session), false, empty(), empty()); + + assert_ok!(RollingWindow::report_misbehavior(Kind::Two, two, current_session)); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 3); + + current_session += 1; + + RollingWindow::on_new_session((current_session - 1, current_session), false, empty(), empty()); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 3); + + current_session += 1; + RollingWindow::on_new_session((current_session - 1, current_session), false, empty(), empty()); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 3); + + current_session += 1; + RollingWindow::on_new_session((current_session - 1, current_session), false, empty(), empty()); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 1); + }); + } + + #[test] + fn bonding_unbounded() { + with_externalities(&mut new_test_ext(), || { + let zero = H256::zero(); + let one: H256 = [1_u8; 32].into(); + + assert_ok!(RollingWindow::report_misbehavior(Kind::Two, zero, 0)); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 1); + assert_noop!(RollingWindow::report_misbehavior(Kind::One, zero, 0), ()); + assert_ok!(RollingWindow::report_misbehavior(Kind::Two, one, 0)); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Two), 2); + + // rolling window has expired but bonding_uniqueness shall be unbounded + RollingWindow::on_new_session((50, 51), false, empty(), empty()); + + assert_noop!(RollingWindow::report_misbehavior(Kind::Two, zero, 51), ()); + assert_noop!(RollingWindow::report_misbehavior(Kind::One, one, 52), ()); + }); + } + + #[test] + fn rolling_window_wrapped() { + with_externalities(&mut new_test_ext(), || { + // window length is u32::max_value should expire at session 24 + assert_ok!(RollingWindow::report_misbehavior(Kind::Four, H256::zero(), 25)); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Four), 1); + + // `u32::max_value() - 25` sessions have been executed + RollingWindow::on_new_session((u32::max_value(), 0), false, empty(), empty()); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Four), 1); + + for session in 0..24 { + RollingWindow::on_new_session((session, session + 1), false, empty(), empty()); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Four), 1); + } + + // `u32::max_value` sessions have been executed should removed from the window + RollingWindow::on_new_session((24, 25), false, empty(), empty()); + assert_eq!(RollingWindow::get_misbehaviors(Kind::Four), 0); + }); + } + + #[test] + fn macros() { + use rstd::marker::PhantomData; + + struct Bar; + + struct Foo(PhantomData<(T, U)>); + + impl_base_severity!(Bar, usize: 1); + impl_base_severity!(Foo, usize: 1337); + impl_kind!(Bar, Kind: Kind::One); + impl_kind!(Foo, Kind: Kind::Two); + + assert_eq!(Bar::base_severity(), 1); + assert_eq!(Foo::::base_severity(), 1337); + assert_eq!(Bar::kind(), Kind::One); + assert_eq!(Foo::::kind(), Kind::Two); + } +} diff --git a/srml/rolling-window/src/mock.rs b/srml/rolling-window/src/mock.rs new file mode 100644 index 0000000000000..c8b48b491f9c0 --- /dev/null +++ b/srml/rolling-window/src/mock.rs @@ -0,0 +1,178 @@ +// 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 . + +use super::*; +use serde::{Serialize, Deserialize}; +use sr_primitives::{Perbill, traits::{BlakeTwo256, IdentityLookup, Convert}, testing::{Header, UintAuthorityId}}; +use substrate_primitives::{Blake2Hasher, H256}; +use srml_support::{impl_outer_origin, parameter_types, traits::{Get, WindowLength}}; +use std::{collections::HashSet, cell::RefCell}; +use parity_codec::{Encode, Decode}; + +pub type AccountId = u64; +pub type Balance = u64; +pub type BlockNumber = u64; +pub type Staking = srml_staking::Module; + +pub struct CurrencyToVoteHandler; + +thread_local! { + static NEXT_VALIDATORS: RefCell> = RefCell::new(vec![1, 2, 3]); + static SESSION: RefCell<(Vec, HashSet)> = RefCell::new(Default::default()); + static EXISTENTIAL_DEPOSIT: RefCell = RefCell::new(0); +} + +impl Convert for CurrencyToVoteHandler { + fn convert(x: u64) -> u64 { x } +} +impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u64 { + x as u64 + } +} + +pub struct ExistentialDeposit; +impl Get for ExistentialDeposit { + fn get() -> u64 { + 0 + } +} + +#[derive(Debug, Copy, Clone, Encode, Decode, Serialize, Deserialize, PartialEq)] +pub enum Kind { + One, + Two, + Three, + Four, +} + +impl WindowLength for Kind { + fn window_length(&self) -> &u32 { + match self { + Kind::One => &4, + Kind::Two => &3, + Kind::Three => &2, + Kind::Four => &u32::max_value(), + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Test; + +impl_outer_origin!{ + pub enum Origin for Test {} +} + +impl system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type WeightMultiplierUpdate = (); + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; +} + +impl balances::Trait for Test { + type Balance = Balance; + type OnFreeBalanceZero = Staking; + type OnNewAccount = (); + type Event = (); + type TransactionPayment = (); + type TransferPayment = (); + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; + type TransactionBaseFee = TransactionBaseFee; + type TransactionByteFee = TransactionByteFee; + type WeightToFee = (); +} + +impl srml_staking::Trait for Test { + type Currency = balances::Module; + type CurrencyToVote = CurrencyToVoteHandler; + type OnRewardMinted = (); + type Event = (); + type Slash = (); + type Reward = (); + type SessionsPerEra = SessionsPerEra; + type BondingDuration = BondingDuration; + type SessionInterface = Self; + type Time = timestamp::Module; +} + +impl srml_session::Trait for Test { + type SelectInitialValidators = Staking; + type OnSessionEnding = Staking; + type Keys = UintAuthorityId; + type ShouldEndSession = srml_session::PeriodicSessions; + type SessionHandler = (); + type Event = (); + type ValidatorId = AccountId; + type ValidatorIdOf = srml_staking::StashOf; +} + +impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} + +impl Trait for Test { + type MisbehaviorKind = Kind; + type SessionKey = UintAuthorityId; +} + +impl srml_session::historical::Trait for Test { + type FullIdentification = srml_staking::Exposure; + type FullIdentificationOf = srml_staking::ExposureOf; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; + pub const SessionsPerEra: srml_session::SessionIndex = 3; + pub const BondingDuration: srml_staking::EraIndex = 3; + pub const Period: BlockNumber = 1; + pub const Offset: BlockNumber = 0; + pub const TransferFee: u64 = 0; + pub const CreationFee: u64 = 0; + pub const TransactionBaseFee: u64 = 0; + pub const TransactionByteFee: u64 = 0; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; +} + +pub fn new_test_ext() -> runtime_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + + srml_session::GenesisConfig:: { + keys: NEXT_VALIDATORS.with(|l| + l.borrow().iter().cloned().map(|i| (i, UintAuthorityId(i))).collect() + ), + }.assimilate_storage(&mut t.0, &mut t.1).unwrap(); + runtime_io::TestExternalities::new_with_children(t) +} diff --git a/srml/session/src/lib.rs b/srml/session/src/lib.rs index a189a91da2848..ae7c6d67f2087 100644 --- a/srml/session/src/lib.rs +++ b/srml/session/src/lib.rs @@ -186,6 +186,7 @@ impl OnSessionEnding for () { pub trait SessionHandler { /// Session set has changed; act appropriately. fn on_new_session( + session_index: (SessionIndex, SessionIndex), changed: bool, validators: &[(ValidatorId, Ks)], queued_validators: &[(ValidatorId, Ks)], @@ -200,15 +201,25 @@ pub trait OneSessionHandler { /// The key type expected. type Key: Decode + Default + TypedKey; - fn on_new_session<'a, I: 'a>(changed: bool, validators: I, queued_validators: I) - where I: Iterator, ValidatorId: 'a; + fn on_new_session<'a, I: 'a>( + session_index: (SessionIndex, SessionIndex), + changed: bool, + validators: I, + queued_validators: I + ) where I: Iterator, ValidatorId: 'a; + fn on_disabled(i: usize); } macro_rules! impl_session_handlers { () => ( impl SessionHandler for () { - fn on_new_session(_: bool, _: &[(AId, Ks)], _: &[(AId, Ks)]) {} + fn on_new_session( + _: (SessionIndex, SessionIndex), + _: bool, + _: &[(AId, Ks)], + _: &[(AId, Ks)], + ) {} fn on_disabled(_: usize) {} } ); @@ -216,6 +227,7 @@ macro_rules! impl_session_handlers { ( $($t:ident)* ) => { impl ),*> SessionHandler for ( $( $t , )* ) { fn on_new_session( + session_index: (SessionIndex, SessionIndex), changed: bool, validators: &[(AId, Ks)], queued_validators: &[(AId, Ks)], @@ -227,7 +239,7 @@ macro_rules! impl_session_handlers { let queued_keys: Box> = Box::new(queued_validators.iter() .map(|k| (&k.0, k.1.get::<$t::Key>(<$t::Key as TypedKey>::KEY_TYPE) .unwrap_or_default()))); - $t::on_new_session(changed, our_keys, queued_keys); + $t::on_new_session(session_index, changed, our_keys, queued_keys); )* } fn on_disabled(i: usize) { @@ -452,7 +464,12 @@ impl Module { Self::deposit_event(Event::NewSession(session_index)); // Tell everyone about the new session keys. - T::SessionHandler::on_new_session::(changed, &session_keys, &queued_amalgamated); + T::SessionHandler::on_new_session::( + (session_index - 1, session_index), + changed, + &session_keys, + &queued_amalgamated, + ); } /// Disable the validator of index `i`. diff --git a/srml/session/src/mock.rs b/srml/session/src/mock.rs index 734f5bbde4bd6..2d7e380a95222 100644 --- a/srml/session/src/mock.rs +++ b/srml/session/src/mock.rs @@ -52,6 +52,7 @@ impl ShouldEndSession for TestShouldEndSession { pub struct TestSessionHandler; impl SessionHandler for TestSessionHandler { fn on_new_session( + (_ended_index, _new_index): (SessionIndex, SessionIndex), changed: bool, validators: &[(u64, T)], _queued_validators: &[(u64, T)], diff --git a/srml/staking/Cargo.toml b/srml/staking/Cargo.toml index 5feab0f5162da..9104af9113ba0 100644 --- a/srml/staking/Cargo.toml +++ b/srml/staking/Cargo.toml @@ -22,6 +22,7 @@ primitives = { package = "substrate-primitives", path = "../../core/primitives" balances = { package = "srml-balances", path = "../balances" } timestamp = { package = "srml-timestamp", path = "../timestamp" } rand = "0.6.5" +srml-rolling-window = { path = "../rolling-window", default-features = false } [features] equalize = [] diff --git a/srml/staking/src/lib.rs b/srml/staking/src/lib.rs index dd540e78b1f36..9a07177e3a1c7 100644 --- a/srml/staking/src/lib.rs +++ b/srml/staking/src/lib.rs @@ -277,19 +277,21 @@ mod tests; mod phragmen; mod inflation; +pub mod slash; #[cfg(all(feature = "bench", test))] mod benches; #[cfg(feature = "std")] use runtime_io::with_storage; -use rstd::{prelude::*, result, collections::btree_map::BTreeMap}; +use rstd::{prelude::*, result, collections::{btree_map::BTreeMap}}; use parity_codec::{HasCompact, Encode, Decode}; use srml_support::{ StorageValue, StorageMap, EnumerableStorageMap, decl_module, decl_event, decl_storage, ensure, traits::{ Currency, OnFreeBalanceZero, OnDilution, LockIdentifier, LockableCurrency, - WithdrawReasons, WithdrawReason, OnUnbalanced, Imbalance, Get, Time + WithdrawReasons, WithdrawReason, OnUnbalanced, Imbalance, Get, Time, + AfterSlash, } }; use session::{historical::OnSessionEnding, SelectInitialValidators, SessionIndex}; @@ -544,7 +546,6 @@ pub trait Trait: system::Trait { decl_storage! { trait Store for Module as Staking { - /// The ideal number of staking participants. pub ValidatorCount get(validator_count) config(): u32; /// Minimum number of staking participants before emergency conditions are imposed. @@ -1504,3 +1505,34 @@ impl SelectInitialValidators for Module { >::select_validators().1 } } + +/// A type for taking action after slashing and rewarding occurred +pub struct AfterSlashing(rstd::marker::PhantomData); + +/// Update state after slashing occurred +impl AfterSlash for AfterSlashing +where + Who: IntoIterator>)>, +{ + fn after_slash(misbehaved: Who, level: u8) { + for (who, exposure) in misbehaved { + // Remove from the validator from next NPoS election + >::remove(&who); + + // Disable the validator + if level >= 2 { + let _ = T::SessionInterface::disable_validator(&who); + } + + // Remove the validator from nominators' lists of trusted candidates + // TODO(niklasad1): not sure if this is correct + if level >= 3 { + for nominator in &exposure.others { + // use `retain` to make sure that duplicated entries are removed too + >::mutate(&nominator.who, |validators| validators.retain(|v| v != &who)); + } + >::remove(&who); + } + } + } +} diff --git a/srml/staking/src/mock.rs b/srml/staking/src/mock.rs index 344ef70e3b6f4..e5763fe4937b6 100644 --- a/srml/staking/src/mock.rs +++ b/srml/staking/src/mock.rs @@ -22,12 +22,15 @@ use sr_primitives::traits::{IdentityLookup, Convert, OpaqueKeys, OnInitialize}; use sr_primitives::testing::{Header, UintAuthorityId}; use primitives::{H256, Blake2Hasher}; use runtime_io; +use session::SessionIndex; use srml_support::{assert_ok, impl_outer_origin, parameter_types, EnumerableStorageMap}; use srml_support::traits::{Currency, Get, FindAuthor}; use crate::{ EraIndex, GenesisConfig, Module, Trait, StakerStatus, ValidatorPrefs, RewardDestination, Nominators, inflation }; +use parity_codec::{Encode, Decode}; +use serde::{Serialize, Deserialize}; /// The AccountId alias in this test module. pub type AccountId = u64; @@ -53,6 +56,7 @@ thread_local! { pub struct TestSessionHandler; impl session::SessionHandler for TestSessionHandler { fn on_new_session( + _session_index: (SessionIndex, SessionIndex), _changed: bool, validators: &[(AccountId, Ks)], _queued_validators: &[(AccountId, Ks)], @@ -71,8 +75,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)) } @@ -194,6 +198,10 @@ impl Trait for Test { type SessionInterface = Self; } +impl srml_rolling_window::Trait for Test { + type SessionKey = UintAuthorityId; +} + pub struct ExtBuilder { existential_deposit: u64, validator_pool: bool, diff --git a/srml/staking/src/slash.rs b/srml/staking/src/slash.rs new file mode 100644 index 0000000000000..e92805c3ffaf2 --- /dev/null +++ b/srml/staking/src/slash.rs @@ -0,0 +1,919 @@ +// 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 . + +//! Slashing mod +//! +//! This is currently located in `Staking` because it has dependency to `Exposure` + +use crate::Exposure; +use srml_support::{ + EnumerableStorageMap, StorageMap, decl_module, decl_storage, + traits::{Currency, DoSlash, DoRewardSlashReporter} +}; +use parity_codec::{HasCompact, Codec, Decode, Encode}; +use rstd::{prelude::*, vec::Vec, collections::{btree_map::BTreeMap, btree_set::BTreeSet}}; +use sr_primitives::{Perbill, traits::{MaybeSerializeDebug, Zero}}; + +type Timestamp = u128; +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +type Id = [u8; 16]; + +/// Slashing trait +pub trait Trait: system::Trait { + /// Currency + type Currency: Currency; +} + +/// Slashed amount for a entity including its nominators +#[derive(Encode, Decode, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct SlashAmount +where + AccountId: Default + Ord, + Balance: Default + HasCompact, +{ + own: Balance, + others: BTreeMap, +} + +/// A misconduct kind with timestamp when it occurred +#[derive(Encode, Decode, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct MisconductsByTime(Vec<(Timestamp, Id)>); + +impl MisconductsByTime { + fn contains_kind(&self, id: Id) -> bool { + self.0.iter().any(|(_, k)| *k == id) + } + + fn multiple_misbehaviors_at_same_time(&self, time: Timestamp, id: Id) -> bool { + self.0.iter().any(|(t, k)| *t == time && *k != id) + } +} + +/// State of a validator +#[derive(Encode, Decode, Default)] +#[cfg_attr(feature = "std", derive(Debug))] +pub struct ValidatorState +where + AccountId: Default + Ord, + Balance: Default + HasCompact, +{ + /// The misconducts the validator has conducted + // TODO: replace this with BTreeSet sorted ordered by latest timestamp...smallest + misconducts: MisconductsByTime, + /// The rewards that the validator has received + rewards: Vec<(Timestamp, Balance)>, + /// Its own balance and the weight of the nominators that supports the validator + exposure: Exposure, + /// The slashed amounts both for the validator and its nominators + slashed_amount: SlashAmount, +} + +decl_storage! { + trait Store for Module as RollingWindow { + /// Slashing history for a given validator + SlashHistory get(misbehavior_reports): linked_map T::AccountId => + ValidatorState>; + } +} + +decl_module! { + /// Slashing module + pub struct Module for enum Call where origin: T::Origin {} +} + +impl Module { + + /// Tries to adjust the `slash` based on `new_slash` and `prev_slash` + /// + /// Returns the total slashed amount + fn adjust_slash( + who: &T::AccountId, + new_slash: BalanceOf, + prev_slash: BalanceOf, + slashed_amount: &mut BalanceOf, + ) -> BalanceOf { + if new_slash > prev_slash { + let amount = new_slash - prev_slash; + T::Currency::slash(&who, amount); + *slashed_amount = *slashed_amount + amount; + new_slash + } else { + prev_slash + } + } + + /// Updates the state of an existing validator which implies updating exposure and + /// update the slashable amount + fn update_known_validator( + who: &T::AccountId, + exposure: Exposure>, + severity: Perbill, + kind: Id, + timestamp: u128, + total_slash: &mut BalanceOf, + ) { + >::mutate(who, |mut state| { + let new_slash = severity * exposure.own; + + Self::adjust_slash(who, new_slash, state.slashed_amount.own, total_slash); + + let intersection: BTreeSet = exposure.others + .iter() + .filter_map(|e1| state.exposure.others.iter().find(|e2| e1.who == e2.who)) + .map(|e| e.who.clone()) + .collect(); + + let previous_slash = rstd::mem::replace(&mut state.slashed_amount.others, BTreeMap::new()); + + for nominator in &exposure.others { + let new_slash = severity * nominator.value; + + // make sure that we are not double slashing + let prev = if intersection.contains(&nominator.who) { + previous_slash.get(&nominator.who).cloned().unwrap_or_else(Zero::zero) + } else { + Zero::zero() + }; + + Self::adjust_slash(&nominator.who, new_slash, prev, total_slash); + state.slashed_amount.others.insert(nominator.who.clone(), new_slash); + } + + state.misconducts.0.push((timestamp, kind)); + state.exposure = exposure; + state.slashed_amount.own = new_slash; + }); + } + + /// Inserts a new validator in the slashing history and applies the slash + fn insert_new_validator( + who: T::AccountId, + exposure: Exposure>, + severity: Perbill, + kind: Id, + timestamp: u128, + total_slash: &mut BalanceOf, + ) { + let amount = severity * exposure.own; + Self::adjust_slash(&who, amount, Zero::zero(), total_slash); + let mut slashed_amount = SlashAmount { own: amount, others: BTreeMap::new() }; + + for nominator in &exposure.others { + let amount = severity * nominator.value; + Self::adjust_slash(&nominator.who, amount, Zero::zero(), total_slash); + slashed_amount.others.insert(nominator.who.clone(), amount); + } + + >::insert(who, ValidatorState { + misconducts: MisconductsByTime(vec![(timestamp, kind)]), + rewards: Vec::new(), + exposure: exposure, + slashed_amount, + }); + } + + /// Updates the history of slashes based on the new severity and only apply new slash + /// if the estimated `slash_amount` exceeds the `previous slash_amount` + /// + /// Returns the `true` if `who` was already in the history otherwise `false` + fn mutate_slash_history( + who: &T::AccountId, + exposure: &Exposure>, + severity: Perbill, + kind: Id, + slashed_entries: &mut Vec<(T::AccountId, Exposure>)>, + total_slash: &mut BalanceOf, + ) -> bool { + let mut in_history = false; + + for (other_who, _) in >::enumerate() { + >::mutate(&other_who, |mut state| { + if state.misconducts.contains_kind(kind) { + if &other_who == who { + in_history = true; + } else { + slashed_entries.push((other_who.clone(), exposure.clone())); + state.slashed_amount.own = Self::adjust_slash( + &other_who, + severity * state.exposure.own, + state.slashed_amount.own, + total_slash + ); + + for nominator in &state.exposure.others { + let new_slash = severity * nominator.value; + if let Some(prev) = state.slashed_amount.others.get_mut(&nominator.who) { + *prev = Self::adjust_slash(&nominator.who, new_slash, *prev, total_slash); + } else { + Self::adjust_slash(&nominator.who, new_slash, Zero::zero(), total_slash); + state.slashed_amount.others.insert(nominator.who.clone(), new_slash); + } + } + } + } + }); + } + + in_history + } +} + +impl DoSlash<(T::AccountId, Exposure>), Perbill, Id, u128> for Module +{ + type SlashedEntries = Vec<(T::AccountId, Exposure>)>; + type SlashedAmount = BalanceOf; + + fn do_slash( + (who, exposure): (T::AccountId, Exposure>), + severity: Perbill, + kind: Id, + timestamp: u128, + ) -> Result<(Self::SlashedEntries, Self::SlashedAmount), ()> { + + // mutable state + let mut slashed_entries: Vec<(T::AccountId, Exposure>)> = Vec::new(); + let mut total_slash = Zero::zero(); + + let who_exist = >::mutate_slash_history( + &who, + &exposure, + severity, + kind, + &mut slashed_entries, + &mut total_slash, + ); + + let seve = if >::get(&who).misconducts.multiple_misbehaviors_at_same_time(timestamp, kind) { + Perbill::one() + } else { + severity + }; + + if who_exist { + Self::update_known_validator(&who, exposure.clone(), seve, kind, timestamp, &mut total_slash); + } else { + Self::insert_new_validator(who.clone(), exposure.clone(), seve, kind, timestamp, &mut total_slash); + } + + slashed_entries.push((who, exposure)); + Ok((slashed_entries, total_slash)) + } +} + +impl DoRewardSlashReporter, u128> for Module +where + Reporters: IntoIterator, +{ + fn do_reward(reporters: Reporters, reward: BalanceOf, timestamp: u128) -> Result<(), ()> { + let mut reward_pot = reward; + + for (reporter, fraction) in reporters { + let amount = rstd::cmp::min(fraction * reward, reward_pot); + reward_pot -= amount; + // This will fail if the account is not existing ignore it for now + if T::Currency::deposit_into_existing(&reporter, amount).is_ok() { + >::mutate(reporter, |state| state.rewards.push((timestamp, amount))); + } + + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + Exposure, IndividualExposure, Validators, + slash::{Trait, Module as SlashingModule}, + mock::* + }; + use rstd::cell::RefCell; + use runtime_io::with_externalities; + use sr_primitives::{Perbill, traits::Hash}; + use srml_rolling_window::{ + Module as RollingWindow, MisbehaviorReporter, GetMisbehaviors, impl_base_severity, impl_kind + }; + use srml_support::{assert_ok, traits::{ReportSlash, DoSlash, AfterSlash, KeyOwnerProofSystem, SlashingOffence}}; + use std::collections::HashMap; + use std::marker::PhantomData; + use primitives::H256; + + type Balances = balances::Module; + + thread_local! { + static EXPOSURES: RefCell>> = + RefCell::new(Default::default()); + static CURRENT_TIME: RefCell = RefCell::new(0); + static CURRENT_KIND: RefCell<[u8; 16]> = RefCell::new([0; 16]); + } + + /// Trait for reporting slashes + pub trait ReporterTrait: srml_rolling_window::Trait + Trait { + /// Key that identifies the owner + type KeyOwner: KeyOwnerProofSystem; + + /// Report of the misconduct + type Reporter; + + /// Slash + type BabeEquivocation: ReportSlash< + Self::Hash, + Self::Reporter, + <::KeyOwner as KeyOwnerProofSystem>::FullIdentification, + u128, + >; + } + + impl Trait for Test { + type Currency = Balances; + } + + impl ReporterTrait for Test { + type KeyOwner = FakeProver; + type BabeEquivocation = BabeEquivocation< + Self, SlashingModule, SlashingModule, crate::AfterSlashing + >; + type Reporter = Vec<(u64, Perbill)>; + } + + #[derive(Debug, Clone, Encode, Decode, PartialEq)] + pub struct FakeProof { + first_header: H, + second_header: H, + author: AccountId, + membership_proof: Proof, + } + + impl FakeProof, AccountId> { + fn new(author: AccountId) -> Self { + Self { + first_header: Default::default(), + second_header: Default::default(), + author, + membership_proof: Vec::new() + } + } + } + + pub struct FakeProver(PhantomData); + + impl KeyOwnerProofSystem for FakeProver { + type Proof = Vec; + type FullIdentification = (u64, Exposure); + + fn prove(_who: u64) -> Option { + Some(Vec::new()) + } + + fn check_proof(who: u64, _proof: Self::Proof) -> Option { + if let Some(exp) = EXPOSURES.with(|x| x.borrow().get(&who).cloned()) { + Some((who, exp)) + } else { + None + } + } + } + + pub struct BabeEquivocationReporter(PhantomData); + + impl BabeEquivocationReporter { + + /// Report an equivocation + pub fn report_equivocation( + proof: FakeProof< + T::Hash, + <::KeyOwner as KeyOwnerProofSystem>::Proof, + T::AccountId + >, + reporters: ::Reporter, + timestamp: u128, + ) -> Result<(), ()> { + let identification = match T::KeyOwner::check_proof(proof.author.clone(), proof.membership_proof) { + Some(id) => id, + None => return Err(()), + }; + + // ignore equivocation slot for this test + let nonce = H256::random(); + let footprint = T::Hashing::hash_of(&(0xbabe, proof.author, nonce)); + + T::BabeEquivocation::slash(footprint, reporters, identification, timestamp) + } + } + + /// This should be something similar to `decl_module!` macro + pub struct BabeEquivocation(PhantomData<(T, DS, DR, AS)>); + + impl BabeEquivocation { + pub fn as_misconduct_level(severity: Perbill) -> u8 { + if severity > Perbill::from_percent(10) { + 4 + } else if severity > Perbill::from_percent(1) { + 3 + } else if severity > Perbill::from_rational_approximation(1_u32, 1000_u32) { + 2 + } else { + 1 + } + } + } + + impl SlashingOffence for BabeEquivocation { + const ID: [u8; 16] = [0; 16]; + const WINDOW_LENGTH: u32 = 5; + } + + impl ReportSlash< + T::Hash, + Reporters, + Who, + u128 + > for BabeEquivocation + where + T: ReporterTrait, + DS: DoSlash, + DR: DoRewardSlashReporter, + AS: AfterSlash, + DS::SlashedAmount: rstd::fmt::Debug, + DS::SlashedEntries: rstd::fmt::Debug, + { + fn slash( + footprint: T::Hash, + reporters: Reporters, + who: Who, + timestamp: u128 + ) -> Result<(), ()> { + // kind is supposed to be `const` but in this case it is mocked and we want change it + // in order to test with separate kinds + let kind = get_current_misconduct_kind(); + + RollingWindow::::report_misbehavior(kind, Self::WINDOW_LENGTH, footprint, 0)?; + let num_violations = RollingWindow::::get_misbehaviors(kind); + + // number of validators + let n = 50; + + // example how to estimate severity + // 3k / n^2 + let severity = Perbill::from_rational_approximation(3 * num_violations, n * n); + + let misconduct_level = Self::as_misconduct_level(severity); + let (slashed, total_slash) = DS::do_slash(who, severity, kind, timestamp)?; + + // hard code reward to 10% of the total amount + let reward_amount = Perbill::from_percent(10) * total_slash; + + // the remaining 90% should go somewhere else, perhaps the `treasory module`?! + + // ignore if rewarding failed, because we need still to update the state of the validators + let _ = DR::do_reward(reporters, reward_amount, timestamp); + AS::after_slash(slashed, misconduct_level); + + Ok(()) + } + } + + fn get_current_time() -> u128 { + CURRENT_TIME.with(|t| *t.borrow()) + } + + fn increase_current_time() { + CURRENT_TIME.with(|t| *t.borrow_mut() += 1); + } + + fn get_current_misconduct_kind() -> Id { + CURRENT_KIND.with(|t| *t.borrow()) + } + + fn set_current_misconduct_kind(id: Id) { + CURRENT_KIND.with(|t| *t.borrow_mut() = id); + } + + #[test] + fn slash_should_keep_state_and_increase_slash_for_history_without_nominators() { + let misbehaved: Vec = (0..10).collect(); + let reporter = (99_u64, Perbill::one()); + + with_externalities(&mut ExtBuilder::default() + .build(), + || { + let _ = Balances::make_free_balance_be(&reporter.0, 50); + EXPOSURES.with(|x| { + for who in &misbehaved { + let exp = Exposure { + own: 1000, + total: 1000, + others: Vec::new(), + }; + let _ = Balances::make_free_balance_be(who, 1000); + x.borrow_mut().insert(*who, exp); + } + }); + + + let mut last_slash = 0; + let mut last_balance = 50; + + // after every slash, the slash history and slash that occurred should be included in the reward + for (i, who) in misbehaved.iter().enumerate() { + let i = i as u64; + assert_ok!(BabeEquivocationReporter::::report_equivocation( + FakeProof::new(*who), + vec![reporter], + get_current_time() + ) + ); + let slash = Perbill::from_rational_approximation(3 * (i + 1), 2500_u64) * 1000; + let total_slash = slash + (slash - last_slash) * i; + let reward = Perbill::from_percent(10) * total_slash; + assert_eq!(Balances::free_balance(&reporter.0), last_balance + reward); + last_balance = Balances::free_balance(&reporter.0); + last_slash = slash; + increase_current_time(); + } + + for who in &misbehaved { + assert_eq!(Balances::free_balance(who), 988, "should slash 1.2%"); + } + + }); + } + + #[test] + fn slash_with_nominators_simple() { + let misbehaved = 1; + + let nom_1 = 11; + let nom_2 = 12; + + with_externalities(&mut ExtBuilder::default() + .build(), + || { + let _ = Balances::make_free_balance_be(&nom_1, 10_000); + let _ = Balances::make_free_balance_be(&nom_2, 50_000); + let _ = Balances::make_free_balance_be(&misbehaved, 9_000); + assert_eq!(Balances::free_balance(&misbehaved), 9_000); + assert_eq!(Balances::free_balance(&nom_1), 10_000); + assert_eq!(Balances::free_balance(&nom_2), 50_000); + + EXPOSURES.with(|x| { + let exp = Exposure { + own: 9_000, + total: 11_200, + others: vec![ + IndividualExposure { who: nom_1, value: 1500 }, + IndividualExposure { who: nom_2, value: 700 }, + ], + }; + x.borrow_mut().insert(misbehaved, exp); + }); + + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(misbehaved), vec![], 0)); + + assert_eq!(Balances::free_balance(&misbehaved), 8_990, "should slash 0.12%"); + assert_eq!(Balances::free_balance(&nom_1), 9_999, "should slash 0.12% of exposure not total balance"); + assert_eq!(Balances::free_balance(&nom_2), 50_000, "should slash 0.12% of exposure not total balance"); + }); + } + + #[test] + fn slash_should_keep_state_and_increase_slash_for_history_with_nominators() { + let misbehaved: Vec = (0..3).collect(); + + let nom_1 = 11; + let nom_2 = 12; + + with_externalities(&mut ExtBuilder::default() + .build(), + || { + let _ = Balances::make_free_balance_be(&nom_1, 10_000); + let _ = Balances::make_free_balance_be(&nom_2, 50_000); + + EXPOSURES.with(|x| { + for &who in &misbehaved { + let exp = Exposure { + own: 1000, + total: 1500, + others: vec![ + IndividualExposure { who: nom_1, value: 300 }, + IndividualExposure { who: nom_2, value: 200 }, + ], + }; + let _ = Balances::make_free_balance_be(&who, 1000); + x.borrow_mut().insert(who, exp); + } + }); + + for who in &misbehaved { + assert_eq!(Balances::free_balance(who), 1000); + } + + for who in &misbehaved { + assert_ok!(BabeEquivocationReporter::::report_equivocation( + FakeProof::new(*who), + vec![], + get_current_time() + ) + ); + increase_current_time(); + } + + for who in &misbehaved { + assert_eq!(Balances::free_balance(who), 997, "should slash 0.36%"); + } + // (300 * 0.0036) * 3 = 3 + assert_eq!(Balances::free_balance(&nom_1), 9_997, "should slash 0.36%"); + // (200 * 0.0036) * 3 = 0 + assert_eq!(Balances::free_balance(&nom_2), 50_000, "should slash 0.36%"); + }); + } + + #[test] + fn slash_update_exposure_when_same_validator_gets_slashed_twice() { + let misbehaved = 0; + + let nom_1 = 11; + let nom_2 = 12; + let nom_3 = 13; + + with_externalities(&mut ExtBuilder::default() + .build(), + || { + let _ = Balances::make_free_balance_be(&nom_1, 10_000); + let _ = Balances::make_free_balance_be(&nom_2, 50_000); + let _ = Balances::make_free_balance_be(&nom_3, 5_000); + let _ = Balances::make_free_balance_be(&misbehaved, 1000); + + + let exp1 = Exposure { + own: 1_000, + total: 31_000, + others: vec![ + IndividualExposure { who: nom_1, value: 5_000 }, + IndividualExposure { who: nom_2, value: 25_000 }, + ], + }; + + EXPOSURES.with(|x| x.borrow_mut().insert(misbehaved, exp1)); + + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(misbehaved), vec![], 0)); + + assert_eq!(Balances::free_balance(&misbehaved), 999, "should slash 0.12%"); + assert_eq!(Balances::free_balance(&nom_1), 9_994, "should slash 0.12%"); + assert_eq!(Balances::free_balance(&nom_2), 49_970, "should slash 0.12%"); + assert_eq!(Balances::free_balance(&nom_3), 5_000, "not exposed should not be slashed"); + + let exp2 = Exposure { + own: 999, + total: 16098, + others: vec![ + IndividualExposure { who: nom_1, value: 10_000 }, + IndividualExposure { who: nom_2, value: 100 }, + IndividualExposure { who: nom_3, value: 4_999 }, + ], + }; + + // change exposure for `misbehaved` + EXPOSURES.with(|x| x.borrow_mut().insert(misbehaved, exp2)); + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(misbehaved), vec![], 1)); + + // exposure is 999 so slashed based on that amount but revert previous slash + // -> 999 * 0.0024 = 2, -> 1000 - 2 = 998 + assert_eq!(Balances::free_balance(&misbehaved), 998, "should slash 0.24%"); + assert_eq!(Balances::free_balance(&nom_1), 9_976, "should slash 0.24%"); + assert_eq!(Balances::free_balance(&nom_2), 49_970, "exposed but slash is smaller previous is still valid"); + // exposure is 4999, slash 0.0024 * 4999 -> 11 + // 5000 - 11 = 4989 + assert_eq!(Balances::free_balance(&nom_3), 4_989, "should slash 0.24%"); + }); + } + + // note, this test hooks in to the `staking` and uses its `AfterSlash` implementation + #[test] + fn simple_with_after_slash() { + with_externalities(&mut ExtBuilder::default() + .build(), + || { + let m1 = 11; + let c1 = 10; + let m2 = 21; + let c2 = 20; + let nom = 101; + let exp1 = Staking::stakers(m1); + let exp2 = Staking::stakers(m2); + let initial_balance_m1 = Balances::free_balance(&m1); + let initial_balance_m2 = Balances::free_balance(&m2); + let initial_balance_nom = Balances::free_balance(&nom); + + // m1 (stash) -> c1 (controller) + // m2 (stash) -> c2 (controller) + assert_eq!(Staking::bonded(&m1), Some(c1)); + assert_eq!(Staking::bonded(&m2), Some(c2)); + assert!(>::exists(&m1)); + assert!(>::exists(&m2)); + + assert_eq!( + exp1, + Exposure { total: 1250, own: 1000, others: vec![ IndividualExposure { who: nom, value: 250 }] } + ); + assert_eq!( + exp2, + Exposure { total: 1250, own: 1000, others: vec![ IndividualExposure { who: nom, value: 250 }] } + ); + + EXPOSURES.with(|x| { + x.borrow_mut().insert(m1, exp1); + x.borrow_mut().insert(m2, exp2) + }); + + assert_ok!( + BabeEquivocationReporter::::report_equivocation(FakeProof::new(m1), vec![], get_current_time()) + ); + assert_eq!(Balances::free_balance(&m1), initial_balance_m1 - 1, "should slash 0.12% of 1000"); + assert_eq!(Balances::free_balance(&m2), initial_balance_m2, "no misconducts yet; no slash"); + assert_eq!(Balances::free_balance(&nom), initial_balance_nom, "0.12% of 250 is zero, don't slash anything"); + + assert!(is_disabled(c1), "m1 has misconduct level 2 should be disabled by now"); + assert!(!>::exists(&m1), "m1 is misconducter shall be disregard from next election"); + assert!(!is_disabled(c2), "m2 is not a misconducter; still available"); + assert!(>::exists(&m2), "no misconducts yet; still a candidate"); + + increase_current_time(); + assert_ok!( + BabeEquivocationReporter::::report_equivocation(FakeProof::new(m2), vec![], get_current_time()) + ); + + assert_eq!(Balances::free_balance(&m1), initial_balance_m1 - 2, "should slash 0.24% of 1000"); + assert_eq!(Balances::free_balance(&m2), initial_balance_m2 - 2, "should slash 0.24% of 1000"); + assert_eq!(Balances::free_balance(&nom), initial_balance_nom, "0.12% of 250 is zero, don't slash anything"); + + assert!(is_disabled(c1), "m1 has misconduct level 2 should be disabled by now"); + assert!(!>::exists(&m1), "m1 is misconducter shall be disregard from next election"); + assert!(is_disabled(c2), "m2 has misconduct level 2 should be disabled by now"); + assert!(!>::exists(&m2), "m2 has misconduct level 2 should be disabled by now"); + + // ensure m1 and m2 are still trusted by its nominator + assert_eq!(Staking::nominators(nom).contains(&m1), true); + assert_eq!(Staking::nominators(nom).contains(&m2), true); + increase_current_time(); + + // increase severity to level 3 + // note, this only reports misconducts from `m2` but `m1` should be updated as well. + for _ in 0..10 { + assert_ok!( + BabeEquivocationReporter::::report_equivocation( + FakeProof::new(m2), + vec![], + get_current_time() + ) + ); + increase_current_time(); + } + + // ensure m1 and m2 are not trusted by its nominator anymore + assert_eq!(Staking::nominators(nom).contains(&m1), false); + assert_eq!(Staking::nominators(nom).contains(&m2), false); + + assert_eq!(Staking::stakers(m1).total, 0); + assert_eq!(Staking::stakers(m2).total, 0); + }); + } + + #[test] + fn rewarding() { + with_externalities(&mut ExtBuilder::default() + .build(), + || { + + let m = 0; + let balance = u32::max_value() as u64; + let _ = Balances::make_free_balance_be(&m, balance); + + EXPOSURES.with(|x| x.borrow_mut().insert(m, Exposure { + own: balance, + total: balance, + others: vec![], + })); + + let reporters = vec![ + (1, Perbill::from_percent(50)), + (2, Perbill::from_percent(20)), + (3, Perbill::from_percent(15)), + (4, Perbill::from_percent(10)), + (5, Perbill::from_percent(50)), + ]; + + // reset balance to 1 for the reporter + for who in 1..=5 { + let _ = Balances::make_free_balance_be(&who, 1); + } + + // slashed amount: 5153960 (0,0132 * 4294967295) will be slashed + // 515396 (0.1 * 5153961) will be shared among the reporters + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m), reporters, 0)); + + assert_eq!(Balances::free_balance(&1), 257698 + 1); + assert_eq!(Balances::free_balance(&2), 103079 + 1); + assert_eq!(Balances::free_balance(&3), 77309 + 1); + assert_eq!(Balances::free_balance(&4), 51539 + 1); + assert_eq!(Balances::free_balance(&5), 25771 + 1, "should only get what's left in the pot; 5% not 50%"); + }); + } + + + #[test] + fn severity_is_based_on_kind() { + with_externalities(&mut ExtBuilder::default() + .build(), + || { + + let exp = Exposure { + own: 1_000, + total: 1_000, + others: Vec::new(), + }; + + let m1 = 0; + let m2 = 1; + let _ = Balances::make_free_balance_be(&m1, 1000); + let _ = Balances::make_free_balance_be(&m2, 1000); + + EXPOSURES.with(|x| { + x.borrow_mut().insert(m1, exp.clone()); + x.borrow_mut().insert(m2, exp) + }); + + for t in 0..100 { + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m1), vec![], t)); + } + + assert_eq!(Balances::free_balance(&m1), 880, "should be slashed by 12%"); + assert_eq!(Balances::free_balance(&m2), 1000); + + set_current_misconduct_kind([1; 16]); + + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m1), vec![], 3000)); + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m2), vec![], 3001)); + + assert_eq!(Balances::free_balance(&m1), 878, "should be slashed by severity on Kind::Two"); + assert_eq!(Balances::free_balance(&m2), 998); + }); + } + + #[test] + fn multiple_misbehaviors_at_the_same_time() { + with_externalities(&mut ExtBuilder::default() + .build(), + || { + + let exp = Exposure { + own: 1_000, + total: 1_000, + others: Vec::new(), + }; + + let m1 = 0; + let m2 = 1; + let _ = Balances::make_free_balance_be(&m1, 1000); + let _ = Balances::make_free_balance_be(&m2, 1000); + + EXPOSURES.with(|x| { + x.borrow_mut().insert(m1, exp.clone()); + x.borrow_mut().insert(m2, exp) + }); + + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m1), vec![], 0)); + + assert_eq!(Balances::free_balance(&m1), 999, "should be slashed by 0.12%"); + assert_eq!(Balances::free_balance(&m2), 1000); + + set_current_misconduct_kind([1; 16]); + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m2), vec![], 0)); + assert_eq!(Balances::free_balance(&m1), 999, "should not be slashed be slashed by Kind::Two"); + assert_eq!(Balances::free_balance(&m2), 999, "should be slashed by 0.12%"); + + assert_ok!(BabeEquivocationReporter::::report_equivocation(FakeProof::new(m1), vec![], 0)); + + assert_eq!(Balances::free_balance(&m1), 0, "multiple misbehavior at the same time"); + assert_eq!(Balances::free_balance(&m2), 998, "should be slashed 0.24%"); + }); + } +} diff --git a/srml/support/src/traits.rs b/srml/support/src/traits.rs index 5f1d7c32ef40e..076087406b2cc 100644 --- a/srml/support/src/traits.rs +++ b/srml/support/src/traits.rs @@ -640,3 +640,50 @@ pub trait ChangeMembers { impl ChangeMembers for () { fn change_members(_incoming: &[T], _outgoing: &[T], _new_set: &[T]) {} } + +/// A generic trait for reporting slashing violations. +pub trait ReportSlash { + /// Reports slashing for a misconduct where `to_slash` are the misbehaved entities and + /// `to_reward` are the entities that detected and reported the misbehavior + /// + /// Returns `Ok` if the misconduct was unique otherwise `Err` + fn slash(to_slash: Misbehaved, to_reward: Reporters, footprint: Hash, timestamp: Timestamp) -> Result<(), ()>; +} + +/// A generic trait for enacting slashes +pub trait DoSlash { + /// The slashed entries + type SlashedEntries; + + /// The total slashed amount + type SlashedAmount: Copy + Clone + Codec + SimpleArithmetic; + + /// Performs the actual slashing based on severity + /// + /// Return the slashes entities which may not be the same as `to_slash` + /// because history slashes of the same kind may be included + fn do_slash(to_slash: Misbehaved, severity: Severity, kind: Kind, timestamp: Timestamp) + -> Result<(Self::SlashedEntries, Self::SlashedAmount), ()>; +} + +/// A generic trait for paying out rewards to entities that +/// has reported misbehaviors as basis for slashing +pub trait DoRewardSlashReporter { + /// Payout reward to reporters + fn do_reward(reporters: Reporters, total_reward: Reward, timestamp: Timestamp) -> Result<(), ()>; +} + +/// A generic event handler trait after slashing occured +pub trait AfterSlash { + /// Invoke event handler after slashing occurred. + fn after_slash(who: Who, misconduct_level: MisconductLevel); +} + +/// Trait for representing an unique slashing offence +pub trait SlashingOffence { + /// Unique identifier of the misconduct + const ID: [u8; 16]; + + /// Window length + const WINDOW_LENGTH: u32; +}