mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 21:01:05 +00:00
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:
committed by
Gavin Wood
parent
99f3f07690
commit
6cc4495700
Generated
+32
-1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -119,6 +119,7 @@ pub struct NetworkConfigurationParams {
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum NodeKeyType {
|
||||
Secp256k1,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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(¤t_session, &auth_idx) {
|
||||
let full_identification = T::FullIdentificationOf::convert(validator_id.clone())
|
||||
.expect(
|
||||
"we got the validator_id from current_elected;
|
||||
current_elected is set of currently elected validators;
|
||||
the mapping between the validator id and its full identification should be valid;
|
||||
thus `FullIdentificationOf::convert` can't return `None`;
|
||||
qed",
|
||||
);
|
||||
|
||||
unresponsive.push((validator_id, full_identification));
|
||||
}
|
||||
}
|
||||
|
||||
let validator_set_count = keys.len() as u32;
|
||||
let offence = UnresponsivenessOffence {
|
||||
session_index: current_session,
|
||||
validator_set_count,
|
||||
offenders: unresponsive,
|
||||
};
|
||||
|
||||
T::ReportUnresponsiveness::report_offence(vec![], offence);
|
||||
}
|
||||
|
||||
fn on_disabled(_i: usize) {
|
||||
// ignore
|
||||
}
|
||||
@@ -453,3 +508,74 @@ impl<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%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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![] },
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user