Offences reporting and slashing (#3322)

* Remove offline slashing logic from staking.

* Initial version of reworked offence module, can report offences

* Clean up staking example.

* Commit SlashingOffence

* Force new era on slash.

* Add offenders in the SlashingOffence trait.

* Introduce the ReportOffence trait.

* Rename `Offence`.

* Add on_before_session_ending handler.

* Move offence related stuff under sr-primitives.

* Fix cargo check.

* Import new im-online implementation.

* Adding validator count to historical session storage as it's needed for slash calculations

* Add a comment about offence.

* Add BabeEquivocationOffence

* GrandpaEquivocationOffence

* slash_fraction and fix

* current_era_start_session_index

* UnresponsivnessOffence

* Finalise OnOffenceHandler traits, and stub impl for staking.

* slash_fraction doesn't really need &self

* Note that offenders count is greater than 0

* Add a test to ensure that I got the math right

* Use FullIdentification in offences.

* Use FullIndentification.

* Hook up the offences module.

* Report unresponsive validators

* Make sure eras have the same length.

* Slashing and rewards.

* Fix compilation.

* Distribute rewards.

* Supply validators_count

* Use identificationTuple in Unresponsivness report

* Fix merge.

* Make sure we don't slash if amount is zero.

* We don't return an error from report_offence anymo

* We actually can use vec!

* Prevent division by zero if the reporters is empty

* offence_forces_new_era/nominators_also_get_slashed

* advance_session

* Fix tests.

* Update srml/staking/src/lib.rs

Co-Authored-By: Robert Habermeier <rphmeier@gmail.com>

* slashing_performed_according_exposure

* Check that reporters receive their slice.

* Small clean-up.

* invulnerables_are_not_slashed

* Minor clean ups.

* Improve docs.

* dont_slash_if_fraction_is_zero

* Remove session dependency from offences.

* Introduce sr-staking-primitives

* Move offence under sr_staking_primitives

* rename session_index

* Resolves todos re using SessionIndex

* Fix staking tests.

* Properly scale denominator.

* Fix UnresponsivnessOffence

* Fix compilation.

* Tests for offences.

* Clean offences tests.

* Fix staking doc test.

* Bump spec version

* Fix aura tests.

* Fix node_executor

* Deposit an event on offence.

* Fix compilation of node-runtime

* Remove aura slashing logic.

* Remove HandleReport

* Update docs for timeslot.

* rename with_on_offence_fractions

* Add should_properly_count_offences

* Replace ValidatorIdByIndex with CurrentElectedSet

ValidatorIdByIndex was querying the current_elected set in each call, doing loading (even though its from cache), deserializing and cloning of element.

Instead of this it is more efficient to use `CurrentElectedSet`. As a small bonus, the invariant became a little bit easier: now we just rely on the fact that `keys` and `current_elected` set are of the same length rather than relying on the fact that `validator_id_by_index` would work similar to `<[T]>::get`.

* Clarify babe equivocation

* Fix offences.

* Rename validators_count to validator_set_count

* Fix squaring.

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: Gavin Wood <gavin@parity.io>

* Docs for CurrentElectedSet.

* Don't punish only invulnerables

* Use `get/insert` instead of `mutate`.

* Fix compilation

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: Gavin Wood <gavin@parity.io>

* Update srml/offences/src/lib.rs

Co-Authored-By: Robert Habermeier <rphmeier@gmail.com>

* Update srml/im-online/src/lib.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update srml/im-online/src/lib.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update srml/im-online/src/lib.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update srml/babe/src/lib.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update core/sr-staking-primitives/src/offence.rs

Co-Authored-By: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Add aura todo.

* Allow multiple reports for single offence report.

* Fix slash_fraction calculation.

* Fix typos.

* Fix compilation and tests.

* Fix staking tests.

* Update srml/im-online/src/lib.rs

Co-Authored-By: Logan Saether <x@logansaether.com>

* Fix doc on time_slot

* Allow slashing only on current era (#3411)

* only slash in current era

* prune journal for last era

* comment own_slash

* emit an event when old slashing events are discarded

* Pave the way for pruning

* Address issues.

* Try to refactor collect_offence_reports

* Other fixes.

* More fixes.
This commit is contained in:
Tomasz Drwięga
2019-08-16 19:54:50 +02:00
committed by Gavin Wood
parent 99f3f07690
commit 6cc4495700
37 changed files with 1775 additions and 597 deletions
+32 -1
View File
@@ -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",
+2
View File
@@ -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",
+1
View File
@@ -119,6 +119,7 @@ pub struct NetworkConfigurationParams {
}
arg_enum! {
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum NodeKeyType {
Secp256k1,
+1
View File
@@ -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.
+31
View File
@@ -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<N> ops::Mul<N> for Perbill
@@ -959,4 +971,23 @@ mod tests {
((Into::<U256>::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),
);
}
}
}
@@ -0,0 +1,18 @@
[package]
name = "sr-staking-primitives"
version = "2.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
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",
]
@@ -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<ValidatorId> {
/// Returns the validator ids for the currently elected validator set.
fn current_elected_set() -> Vec<ValidatorId>;
}
@@ -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 <http://www.gnu.org/licenses/>.
//! 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<Offender> {
/// 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<Offender>;
/// 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<Reporter, Offender, O: Offence<Offender>> {
/// Report an `offence` and reward given `reporters`.
fn report_offence(reporters: Vec<Reporter>, offence: O);
}
impl<Reporter, Offender, O: Offence<Offender>> ReportOffence<Reporter, Offender, O> for () {
fn report_offence(_reporters: Vec<Reporter>, _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<Reporter, Offender> {
/// 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<Reporter, Offender>],
slash_fraction: &[Perbill],
);
}
impl<Reporter, Offender> OnOffenceHandler<Reporter, Offender> for () {
fn on_offence(
_offenders: &[OffenceDetails<Reporter, Offender>],
_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<Reporter, Offender> {
/// 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<Reporter>,
}
@@ -143,7 +143,6 @@ impl system::Trait for Runtime {
}
impl aura::Trait for Runtime {
type HandleReport = ();
type AuthorityId = AuraId;
}
+5 -6
View File
@@ -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 {
+1 -2
View File
@@ -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()
}),
+22 -18
View File
@@ -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",
]
+12 -3
View File
@@ -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<Runtime>;
}
impl offences::Trait for Runtime {
type Event = Event;
type IdentificationTuple = session::historical::IdentificationTuple<Self>;
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},
}
);
+16 -18
View File
@@ -5,39 +5,37 @@ authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
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 }
rstd = { package = "sr-std", path = "../../core/sr-std", 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 }
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
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 }
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",
"rstd/std",
"srml-support/std",
"sr-primitives/std",
"inherents/std",
"runtime_io/std",
"primitives/std",
"rstd/std",
"serde",
"sr-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",
]
+4 -72
View File
@@ -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<T: Trait> IsMember<T::AuthorityId> for Module<T> {
}
}
/// 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<F>(&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<T: Trait> Module<T> {
/// Determine the Aura slot-duration based on the Timestamp module configuration.
pub fn slot_duration() -> T::Moment {
@@ -281,7 +238,7 @@ impl<T: Trait> Module<T> {
<T as timestamp::Trait>::MinimumPeriod::get().saturating_mul(2.into())
}
fn on_timestamp_set<H: HandleReport>(now: T::Moment, slot_duration: T::Moment) {
fn on_timestamp_set(now: T::Moment, slot_duration: T::Moment) {
let last = Self::last();
<Self as Store>::LastTimestamp::put(now.clone());
@@ -292,42 +249,17 @@ impl<T: Trait> Module<T> {
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::<usize>(),
skipped: skipped_slots.saturated_into::<usize>(),
})
// TODO [#3398] Generate offence report for all authorities that skipped their slots.
}
}
impl<T: Trait> OnTimestampSet<T::Moment> for Module<T> {
fn on_timestamp_set(moment: T::Moment) {
Self::on_timestamp_set::<T::HandleReport>(moment, Self::slot_duration())
}
}
/// A type for performing slashing based on Aura reports.
pub struct StakingSlasher<T>(::rstd::marker::PhantomData<T>);
impl<T: staking::Trait + Trait> HandleReport for StakingSlasher<T> {
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::<T>::on_offline_validator(v, slash_count);
}
);
Self::on_timestamp_set(moment, Self::slot_duration())
}
}
-2
View File
@@ -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<u64>) -> runtime_io::TestExternalities<Blak
t.into()
}
pub type System = system::Module<Test>;
pub type Aura = Module<Test>;
+4 -66
View File
@@ -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<Vec<usize>> = 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::<HandleTestReport>(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::<HandleTestReport>(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);
});
}
+2
View File
@@ -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",
+56 -2
View File
@@ -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<T: Trait> session::ShouldEndSession<T::BlockNumber> for Module<T> {
}
}
// 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<FullIdentification> {
/// 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<FullIdentification: Clone> Offence<FullIdentification> for BabeEquivocationOffence<FullIdentification> {
const ID: Kind = *b"babe:equivocatio";
type TimeSlot = u64;
fn offenders(&self) -> Vec<FullIdentification> {
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<T: Trait> Module<T> {
/// Determine the BABE slot duration based on the Timestamp module configuration.
pub fn slot_duration() -> T::Moment {
+2
View File
@@ -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",
+58
View File
@@ -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<T: Trait> finality_tracker::OnFinalizationStalled<T::BlockNumber> for Modul
<Stalled<T>>::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<FullIdentification> {
/// 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<FullIdentification: Clone> Offence<FullIdentification> for GrandpaEquivocationOffence<FullIdentification> {
const ID: Kind = *b"grandpa:equivoca";
type TimeSlot = GrandpaTimeSlot;
fn offenders(&self) -> Vec<FullIdentification> {
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()
}
}
+28
View File
@@ -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]));
}
+11 -8
View File
@@ -5,27 +5,30 @@ authors = ["Parity Technologies <admin@parity.io>"]
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",
]
+133 -7
View File
@@ -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<BlockNumber>
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<Event> + Into<<Self as system::Trait>::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=<Self as Trait>::Call> + Encode + Decode;
/// A type that gives us the ability to submit unresponsiveness offence reports.
type ReportUnresponsiveness:
ReportOffence<
Self::AccountId,
IdentificationTuple<Self>,
UnresponsivenessOffence<IdentificationTuple<Self>>,
>;
/// A type that returns a validator id from the current elected set of the era.
type CurrentElectedSet: CurrentElectedSet<<Self as session::Trait>::ValidatorId>;
}
decl_event!(
@@ -382,6 +398,7 @@ impl<T: Trait> Module<T> {
}
impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> {
type Key = AuthorityId;
fn on_genesis_session<'a, I: 'a>(validators: I)
@@ -404,6 +421,44 @@ impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> {
Keys::put(validators.map(|x| x.1).collect::<Vec<_>>());
}
fn on_before_session_ending() {
let mut unresponsive = vec![];
let current_session = <session::Module<T>>::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 !<ReceivedHeartbeats>::exists(&current_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<T: Trait> srml_support::unsigned::ValidateUnsigned for Module<T> {
TransactionValidity::Invalid(0)
}
}
/// An offence that is filed if a validator didn't send a heartbeat message.
pub struct UnresponsivenessOffence<Offender> {
/// 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<Offender>,
}
impl<Offender: Clone> Offence<Offender> for UnresponsivenessOffence<Offender> {
const ID: Kind = *b"im-online:offlin";
type TimeSlot = SessionIndex;
fn offenders(&self) -> Vec<Offender> {
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%
);
}
}
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "srml-offences"
version = "1.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
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",
]
+282
View File
@@ -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 <http://www.gnu.org/licenses/>.
//! # 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<u8>;
/// A type alias for a report identifier.
type ReportIdOf<T> = <T as system::Trait>::Hash;
/// Offences trait
pub trait Trait: system::Trait {
/// The overarching event type.
type Event: From<Event> + Into<<Self as system::Trait>::Event>;
/// Full identification of the validator.
type IdentificationTuple: Parameter + Ord;
/// A handler called for every offence report.
type OnOffenceHandler: OnOffenceHandler<Self::AccountId, Self::IdentificationTuple>;
}
decl_storage! {
trait Store for Module<T: Trait> as Offences {
/// The primary structure that holds all offence records keyed by report identifiers.
Reports get(reports): map ReportIdOf<T> => Option<OffenceDetails<T::AccountId, T::IdentificationTuple>>;
/// A vector of reports of the same kind that happened at the same time slot.
ConcurrentReportsIndex: double_map Kind, blake2_256(OpaqueTimeSlot) => Vec<ReportIdOf<T>>;
/// 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<u8>`, this is because values of
/// different types are not supported at the moment so we are doing the manual serialization.
ReportsByKindIndex: map Kind => Vec<u8>; // (O::TimeSlot, ReportIdOf<T>)
}
}
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<T: Trait> for enum Call where origin: T::Origin {
fn deposit_event() = default;
}
}
impl<T: Trait, O: Offence<T::IdentificationTuple>>
ReportOffence<T::AccountId, T::IdentificationTuple, O> for Module<T>
where
T::IdentificationTuple: Clone,
{
fn report_offence(reporters: Vec<T::AccountId>, 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::<O>(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::<Vec<_>>();
T::OnOffenceHandler::on_offence(&concurrent_offenders, &slash_perbill);
}
}
impl<T: Trait> Module<T> {
/// 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<O: Offence<T::IdentificationTuple>>(
time_slot: &O::TimeSlot,
offender: &T::IdentificationTuple,
) -> ReportIdOf<T> {
(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<O: Offence<T::IdentificationTuple>>(
reporters: Vec<T::AccountId>,
time_slot: &O::TimeSlot,
offenders: Vec<T::IdentificationTuple>,
) -> Option<TriageOutcome<T>> {
let mut storage = ReportIndexStorage::<T, O>::load(time_slot);
let mut new_offenders = BTreeSet::new();
for offender in offenders {
let report_id = Self::report_id::<O>(time_slot, &offender);
if !<Reports<T>>::exists(&report_id) {
new_offenders.insert(offender.clone());
<Reports<T>>::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| <Reports<T>>::get(report_id))
.collect::<Vec<_>>();
storage.save();
Some(TriageOutcome {
new_offenders,
concurrent_offenders,
})
} else {
None
}
}
}
struct TriageOutcome<T: Trait> {
/// Offenders that was spotted in the unique reports.
new_offenders: BTreeSet<T::IdentificationTuple>,
/// Other reports for the same report kinds.
concurrent_offenders: Vec<OffenceDetails<T::AccountId, T::IdentificationTuple>>,
}
/// 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<T: Trait, O: Offence<T::IdentificationTuple>> {
opaque_time_slot: OpaqueTimeSlot,
concurrent_reports: Vec<ReportIdOf<T>>,
same_kind_reports: Vec<(O::TimeSlot, ReportIdOf<T>)>,
}
impl<T: Trait, O: Offence<T::IdentificationTuple>> ReportIndexStorage<T, O> {
/// 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 = <ReportsByKindIndex>::get(&O::ID);
let same_kind_reports =
Vec::<(O::TimeSlot, ReportIdOf<T>)>::decode(&mut &same_kind_reports[..])
.unwrap_or_default();
let concurrent_reports = <ConcurrentReportsIndex<T>>::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<T>) {
// 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) {
<ReportsByKindIndex>::insert(&O::ID, self.same_kind_reports.encode());
<ConcurrentReportsIndex<T>>::insert(
&O::ID,
&self.opaque_time_slot,
&self.concurrent_reports,
);
}
}
+162
View File
@@ -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 <http://www.gnu.org/licenses/>.
//! 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<Vec<Perbill>> = RefCell::new(Default::default());
}
impl<Reporter, Offender> offence::OnOffenceHandler<Reporter, Offender> for OnOffenceHandler {
fn on_offence(
_offenders: &[OffenceDetails<Reporter, Offender>],
slash_fraction: &[Perbill],
) {
ON_OFFENCE_PERBILL.with(|f| {
*f.borrow_mut() = slash_fraction.to_vec();
});
}
}
pub fn with_on_offence_fractions<R, F: FnOnce(&mut Vec<Perbill>) -> 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<Self::AccountId>;
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<Blake2Hasher> {
let t = system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
t.into()
}
/// Offences module.
pub type Offences = Module<Runtime>;
pub type System = system::Module<Runtime>;
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<OffenceDetails<u64, u64>> {
<crate::ConcurrentReportsIndex<Runtime>>::get(&kind, &time_slot.encode())
.into_iter()
.map(|report_id| {
<crate::Reports<Runtime>>::get(&report_id)
.expect("dangling report id is found in ConcurrentReportsIndex")
})
.collect()
}
#[derive(Clone)]
pub struct Offence<T> {
pub validator_set_count: u32,
pub offenders: Vec<T>,
pub time_slot: u128,
}
impl<T: Clone> offence::Offence<T> for Offence<T> {
const ID: offence::Kind = KIND;
type TimeSlot = u128;
fn offenders(&self) -> Vec<T> {
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)
}
}
+246
View File
@@ -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 <http://www.gnu.org/licenses/>.
//! 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![] },
]
);
});
}
+2
View File
@@ -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"
]
+8 -5
View File
@@ -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<T: Trait> as Session {
/// Mapping from historical session indices to session-data root hash.
HistoricalSessions get(historical_root): map SessionIndex => Option<T::Hash>;
/// 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<Vec<(T::ValidatorId, T::FullIdentification)>>;
@@ -121,8 +123,9 @@ impl<T: Trait, I> crate::OnSessionEnding<T::ValidatorId> 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 = <SessionModule<T>>::validators().len() as u32;
match ProvingTrie::<T>::generate_for(ending) {
Ok(trie) => <HistoricalSessions<T>>::insert(ending, &trie.root),
Ok(trie) => <HistoricalSessions<T>>::insert(ending, &(trie.root, count)),
Err(reason) => {
print("Failed to generate historical ancestry-inclusion proof.");
print(reason);
@@ -278,7 +281,7 @@ impl<T: Trait, D: AsRef<[u8]>> srml_support::traits::KeyOwnerProofSystem<(KeyTyp
for Module<T>
{
type Proof = Proof;
type FullIdentification = IdentificationTuple<T>;
type IdentificationTuple = IdentificationTuple<T>;
fn prove(key: (KeyTypeId, D)) -> Option<Self::Proof> {
let session = <SessionModule<T>>::current_index();
@@ -300,7 +303,7 @@ impl<T: Trait, D: AsRef<[u8]>> srml_support::traits::KeyOwnerProofSystem<(KeyTyp
T::FullIdentificationOf::convert(owner.clone()).map(move |id| (owner, id))
)
} else {
let root = <HistoricalSessions<T>>::get(&proof.session)?;
let (root, _) = <HistoricalSessions<T>>::get(&proof.session)?;
let trie = ProvingTrie::<T>::from_nodes(root, &proof.trie_nodes);
trie.query(id, data.as_ref())
+34 -8
View File
@@ -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<BlockNumber> {
/// 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<ValidatorId> {
/// Handle the fact that the session is ending, and optionally provide the new validator set.
///
@@ -185,7 +184,7 @@ impl<A> OnSessionEnding<A> for () {
fn on_session_ending(_: SessionIndex, _: SessionIndex) -> Option<Vec<A>> { None }
}
/// Handler for when a session keys set changes.
/// Handler for session lifecycle events.
pub trait SessionHandler<ValidatorId> {
/// 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<ValidatorId> {
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<ValidatorId> {
/// The key type expected.
type Key: Decode + Default + AppKey;
@@ -212,10 +217,23 @@ pub trait OneSessionHandler<ValidatorId> {
fn on_genesis_session<'a, I: 'a>(validators: I)
where I: Iterator<Item=(&'a ValidatorId, Self::Key)>, ValidatorId: 'a;
fn on_new_session<'a, I: 'a>(changed: bool, validators: I, queued_validators: I)
where I: Iterator<Item=(&'a ValidatorId, Self::Key)>, 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<Item=(&'a ValidatorId, Self::Key)>, 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<AId> SessionHandler<AId> for () {
fn on_genesis_session<Ks: OpaqueKeys>(_: &[(AId, Ks)]) {}
fn on_new_session<Ks: OpaqueKeys>(_: 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);
+1
View File
@@ -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 {
+2
View File
@@ -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",
+190 -137
View File
@@ -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<T: Trait> 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)?;
//! <staking::Module<T>>::on_offline_validator(reported, 1);
//! <staking::Module<T>>::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<Balance: HasCompact> {
/// 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<Balance: HasCompact> {
impl<B: Default + HasCompact + Copy> Default for ValidatorPrefs<B> {
fn default() -> Self {
ValidatorPrefs {
unstake_threshold: 3,
validator_payment: Default::default(),
}
}
@@ -465,6 +441,15 @@ pub struct Exposure<AccountId, Balance: HasCompact> {
pub others: Vec<IndividualExposure<AccountId, Balance>>,
}
/// 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<AccountId, Balance: HasCompact> {
who: AccountId,
amount: Balance,
own_slash: Balance, // the amount of `who`'s own exposure that was slashed
}
pub type BalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
type PositiveImbalanceOf<T> =
@@ -492,7 +477,7 @@ pub trait SessionInterface<AccountId>: system::Trait {
/// Get the validators from session.
fn validators() -> Vec<AccountId>;
/// 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<T: Trait> SessionInterface<<T as system::Trait>::AccountId> for T where
@@ -514,7 +499,7 @@ impl<T: Trait> SessionInterface<<T as system::Trait>::AccountId> for T where
<session::Module<T>>::validators()
}
fn prune_historical_up_to(up_to: session::SessionIndex) {
fn prune_historical_up_to(up_to: SessionIndex) {
<session::historical::Module<T>>::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<T>;
/// 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<SlashJournalEntry<T::AccountId, BalanceOf<T>>>;
}
add_extra_genesis {
config(stakers):
@@ -688,11 +670,11 @@ decl_event!(
pub enum Event<T> where Balance = BalanceOf<T>, <T as system::Trait>::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"
);
<Nominators<T>>::remove(stash);
<Validators<T>>::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<T::AccountId>) {
@@ -1070,20 +1041,42 @@ impl<T: Trait> Module<T> {
<Ledger<T>>::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<T>) {
// 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<T>,
exposure: &Exposure<T::AccountId, BalanceOf<T>>,
journal: &mut Vec<SlashJournalEntry<T::AccountId, BalanceOf<T>>>,
) -> NegativeImbalanceOf<T> {
// 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(<BalanceOf<T>>::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<T: Trait> Module<T> {
}
}
}
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<T: Trait> Module<T> {
fn new_session(session_index: SessionIndex)
-> Option<(Vec<T::AccountId>, Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>)>
{
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<T: Trait> Module<T> {
// Increment current era.
let current_era = CurrentEra::mutate(|s| { *s += 1; *s });
// prune journal for last era.
<EraSlashJournal<T>>::remove(current_era - 1);
CurrentEraStartSessionIndex::mutate(|v| {
*v = start_session_index;
});
@@ -1325,13 +1335,9 @@ impl<T: Trait> Module<T> {
equalize::<T>(&mut assignments_with_votes, &mut exposures, tolerance, iterations);
}
// Clear Stakers and reduce their slash_count.
// Clear Stakers.
for v in Self::current_elected().iter() {
<Stakers<T>>::remove(v);
let slash_count = <SlashCount<T>>::take(v);
if slash_count > 1 {
<SlashCount<T>>::insert(v, slash_count - 1);
}
}
// Populate Stakers and figure out the minimum stake behind a slot.
@@ -1371,68 +1377,10 @@ impl<T: Trait> Module<T> {
<Ledger<T>>::remove(&controller);
}
<Payee<T>>::remove(stash);
<SlashCount<T>>::remove(stash);
<Validators<T>>::remove(stash);
<Nominators<T>>::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;
<SlashCount<T>>::insert(&stash, new_slash_count);
let grace = Self::offline_slash_grace();
if RECENT_OFFLINE_COUNT > 0 {
let item = (stash.clone(), <system::Module<T>>::block_number(), count as u32);
<RecentlyOffline<T>>::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<T: Trait> SelectInitialValidators<T::AccountId> for Module<T> {
<Module<T>>::select_validators().1
}
}
/// This is intended to be used with `FilterHistoricalOffences`.
impl <T: Trait> OnOffenceHandler<T::AccountId, session::historical::IdentificationTuple<T>> for Module<T> where
T: session::Trait<ValidatorId = <T as system::Trait>::AccountId>,
T: session::historical::Trait<
FullIdentification = Exposure<<T as system::Trait>::AccountId, BalanceOf<T>>,
FullIdentificationOf = ExposureOf<T>,
>,
T::SessionHandler: session::SessionHandler<<T as system::Trait>::AccountId>,
T::OnSessionEnding: session::OnSessionEnding<<T as system::Trait>::AccountId>,
T::SelectInitialValidators: session::SelectInitialValidators<<T as system::Trait>::AccountId>,
T::ValidatorIdOf: Convert<<T as system::Trait>::AccountId, Option<<T as system::Trait>::AccountId>>
{
fn on_offence(
offenders: &[OffenceDetails<T::AccountId, session::historical::IdentificationTuple<T>>],
slash_fraction: &[Perbill],
) {
let mut remaining_imbalance = <NegativeImbalanceOf<T>>::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);
}
}
<EraSlashJournal<T>>::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<T, R> {
_inner: rstd::marker::PhantomData<(T, R)>,
}
impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
for FilterHistoricalOffences<Module<T>, R> where
T: Trait,
R: ReportOffence<Reporter, Offender, O>,
O: Offence<Offender>,
{
fn report_offence(reporters: Vec<Reporter>, offence: O) {
// disallow any slashing from before the current era.
let offence_session = offence.session_index();
if offence_session >= <Module<T>>::current_era_start_session_index() {
R::report_offence(reporters, offence)
} else {
<Module<T>>::deposit_event(
RawEvent::OldSlashingReportDiscarded(offence_session).into()
)
}
}
}
/// Returns the currently elected validator set represented by their stash accounts.
pub struct CurrentElectedStashAccounts<T>(rstd::marker::PhantomData<T>);
impl<T: Trait> CurrentElectedSet<T::AccountId> for CurrentElectedStashAccounts<T> {
fn current_elected_set() -> Vec<T::AccountId> {
<Module<T>>::current_elected()
}
}
+20 -8
View File
@@ -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<AccountId> 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<u32>,
invulnerables: Vec<u64>,
}
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<u64>) -> 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::<Test>{
current_era: 0,
stakers: vec![
// (stash, controller, staked_amount, status)
(11, 10, balance_factor * 1000, StakerStatus::<AccountId>::Validator),
(21, 20, stake_21, StakerStatus::<AccountId>::Validator),
(31, 30, stake_31, StakerStatus::<AccountId>::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::<Test> {
@@ -398,7 +405,12 @@ pub fn bond_nominator(acc: u64, val: u64, target: Vec<u64>) {
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 {
+180 -230
View File
@@ -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!(<Validators<Test>>::enumerate().collect::<Vec<_>>(), 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,9 +84,12 @@ 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);
// Account 10 has `balance_factor` free balance
assert_eq!(Balances::free_balance(&10), 1);
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();
@@ -93,25 +97,6 @@ fn basic_setup_works() {
});
}
#[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);
});
}
#[test]
fn change_controller_works() {
with_externalities(&mut ExtBuilder::default().build(),
@@ -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!(<Validators<Test>>::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!(<Validators<Test>>::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!(<Validators<Test>>::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
<Stakers<Test>>::insert(&11, Exposure { total: 1000000, own: 1000000, others: vec![]});
<Stakers<Test>>::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
<Validators<Test>>::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.
<Validators<Test>>::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() {
});
<Payee<Test>>::insert(&2, RewardDestination::Stash);
<Validators<Test>>::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!(<Ledger<Test>>::exists(&10));
assert!(<Bonded<Test>>::exists(&11));
assert!(<Validators<Test>>::exists(&11));
assert!(<SlashCount<Test>>::exists(&11));
assert!(<Payee<Test>>::exists(&11));
// Reduce free_balance of controller to 0
@@ -1389,7 +1195,6 @@ fn on_free_balance_zero_stash_removes_validator() {
assert!(<Ledger<Test>>::exists(&10));
assert!(<Bonded<Test>>::exists(&11));
assert!(<Validators<Test>>::exists(&11));
assert!(<SlashCount<Test>>::exists(&11));
assert!(<Payee<Test>>::exists(&11));
// Reduce free_balance of stash to 0
@@ -1402,7 +1207,6 @@ fn on_free_balance_zero_stash_removes_validator() {
assert!(!<Bonded<Test>>::exists(&11));
assert!(!<Validators<Test>>::exists(&11));
assert!(!<Nominators<Test>>::exists(&11));
assert!(!<SlashCount<Test>>::exists(&11));
assert!(!<Payee<Test>>::exists(&11));
});
}
@@ -1459,7 +1263,6 @@ fn on_free_balance_zero_stash_removes_nominator() {
assert!(!<Bonded<Test>>::exists(&11));
assert!(!<Validators<Test>>::exists(&11));
assert!(!<Nominators<Test>>::exists(&11));
assert!(!<SlashCount<Test>>::exists(&11));
assert!(!<Payee<Test>>::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);
});
}
+3 -3
View File
@@ -117,8 +117,8 @@ pub trait VerifySeal<Header, Author> {
pub trait KeyOwnerProofSystem<Key> {
/// 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<Key> {
/// 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<Self::FullIdentification>;
fn check_proof(key: Key, proof: Self::Proof) -> Option<Self::IdentificationTuple>;
}
/// Handler for when some currency "account" decreased in balance for