grandpa: report equivocations (#3868)

* session: runtime api for generating session membership proofs

* grandpa: add runtime api for creating equivocation report txs

* grandpa: submit signed equivocation report transactions

* grandpa: use proper equivocation report type

* grandpa: report equivocations

* grandpa: validate equivocation proof

* grandpa: update to finality-grandpa 0.9.1

* grandpa: fix encoding of session membership proof

* grandpa: initialize set id session mapping for genesis session

* grandpa: fix bug in set_id session validation

* fix compilation

* cleanup from merge conflicts

* cleanup crate tomls

* grandpa: refactor equivocation handling to separate trait

* node-template: fix compilation

* fix test compilation

* bump finality-grandpa to v0.10.2

* rpc: fix runtime version test

* CHERRY-PICK #4200: Add documentation to SubmitSignedTransaction and actually make it work

Squashed commit of the following:

commit 4f2cb0b1c588a06f2f3b478bb4b28b5cb29d54b9
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Tue Dec 3 16:29:33 2019 +0100

    Split the method to avoid confusing type error message.

commit c5bf24eeaaf902add89ed1b046b22c4a4aaeb2cd
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Tue Dec 3 16:19:55 2019 +0100

    Make accounts optional, fix logic.

commit 97db1ef556e023cf6847e5ffdb036c0e3ea6fb0a
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Tue Dec 3 10:06:20 2019 +0100

    Remove warning.

commit 535f5c116d1a2e826eaf90c3f7e6798e443d61d8
Merge: 516257217 0f1a5f651
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Tue Dec 3 07:08:05 2019 +0100

    Merge branch 'master' into td-signed-transactions

commit 516257217bac89fcebd083712f4ea68b7b23b55a
Merge: ac98248c6 2e68c80c2
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Mon Dec 2 13:57:25 2019 +0100

    Merge branch 'master' into td-signed-transactions

commit ac98248c6c56cff381130645a82a13d29933cf83
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Mon Nov 25 17:34:52 2019 +0100

    Forgotten import.

commit 67a3c19031506c28e31c6bc4a90fff62d467dd58
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Mon Nov 25 17:32:10 2019 +0100

    Fix naming and bounds.

commit 93e768ea9df97a4629fca1f9bc4b108fdb33f876
Author: Tomasz Drwięga <tomasz@parity.io>
Date:   Mon Nov 25 17:01:05 2019 +0100

    Add documentation to signed transactions and actually make them work.

* grandpa: skip block initialization on report submission method

* primitives: allow transaction pool access by default for offchain calls

* grandpa: unused parameters

* grandpa: remove unused method

* grandpa: enable equivocation reporting

* grandpa: add workaround for parameter encoding

* grandpa: fix localized_payload calls in tests

* fix submit_report_equivocation_extrinsic in runtimes

* node: fix submit transaction test compilation

* node: bump spec_version

* rpc: fix api version test

* grandpa: allow custom equivocation offence type

* grandpa: add test for authorities::next_change_height

* grandpa: cleanup report_equivocation function

* node: move reporting app crypto to node-primitives

* grandpa: move equivocation traits to own module

* grandpa: rename app-crypto crate import

* grandpa: export equivocation types

* node: bump spec_version

* grandpa: rename EquivocationReport to EquivocationProof

* grandpa: add missing docs to primitives

* grandpa: add missing docs to equivocation

* node: fix compilation

* grandpa: add missing docs to pallet

* node: bump spec_version

* fix whitespace

* grandpa: return error on offence reporting

* grandpa: expose session and validator count in proofs through traits

* grandpa: use strong key in module KeyOwnerProofSystem

* grandpa: move key ownership proof to grandpa runtime api

* grandpa: remove unnecessary cloning when checking equivocation proof

* grandpa: make report_equivocation a method in Environment

* support: implement KeyOwnerProofSystem for ()

* grandpa: move KeyOwnerProofSystem to module trait

* test-utils: fix runtime compilation

* grandpa: fix test compilation

* grandpa: fix test compilation after merge

* grandpa: simplify transaction submission types

* grandpa: validate equivocation report in signed extension

* client: fix test

* node: use ValidateEquivocationReport signed extension

* grandpa: expose key ownership proof under opaque type

* grandpa: better docs on key ownership proofs

* grandpa: add note about signed extension

* grandpa: add ValidateEquivocationReport::new

* grandpa: remove skip_initialize_block from runtime api

* grandpa: use new offchain transaction submission API

* grandpa: take set_id in generate_key_ownership_proof

* grandpa: update to finality-grandpa v0.12.2

* grandpa: cleanup usages of AuthoritySet::current

* grandpa: fix test

* grandpa: add mocking utilities for equivocation reporting

* grandpa: add test for equivocation reporting

* grandpa: move SetIdSession initialization

* grandpa: add more tests

* node: enable historical session manager

* node: bump spec_version

* node: use strong key types in KeyOwnerProofSystem definitions

* grandpa: export GrandpaEquivocationOffence type
This commit is contained in:
André Silva
2020-05-06 17:25:51 +01:00
committed by GitHub
parent a1127f8f9d
commit fbd2ac8f3b
38 changed files with 2249 additions and 296 deletions
+12 -1
View File
@@ -14,8 +14,10 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
serde = { version = "1.0.101", optional = true, features = ["derive"] }
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] }
sp-application-crypto = { version = "2.0.0-dev", default-features = false, path = "../../primitives/application-crypto" }
sp-core = { version = "2.0.0-dev", default-features = false, path = "../../primitives/core" }
sp-finality-grandpa = { version = "2.0.0-dev", default-features = false, path = "../../primitives/finality-grandpa" }
sp-session = { version = "2.0.0-dev", default-features = false, path = "../../primitives/session" }
sp-std = { version = "2.0.0-dev", default-features = false, path = "../../primitives/std" }
sp-runtime = { version = "2.0.0-dev", default-features = false, path = "../../primitives/runtime" }
sp-staking = { version = "2.0.0-dev", default-features = false, path = "../../primitives/staking" }
@@ -25,15 +27,24 @@ pallet-session = { version = "2.0.0-dev", default-features = false, path = "../s
pallet-finality-tracker = { version = "2.0.0-dev", default-features = false, path = "../finality-tracker" }
[dev-dependencies]
sp-io ={ version = "2.0.0-dev", path = "../../primitives/io" }
grandpa = { package = "finality-grandpa", version = "0.12.2", features = ["derive-codec"] }
sp-io = { version = "2.0.0-dev", path = "../../primitives/io" }
sp-keyring = { version = "2.0.0-dev", path = "../../primitives/keyring" }
pallet-balances = { version = "2.0.0-dev", path = "../balances" }
pallet-offences = { version = "2.0.0-dev", path = "../offences" }
pallet-staking = { version = "2.0.0-dev", path = "../staking" }
pallet-staking-reward-curve = { version = "2.0.0-dev", path = "../staking/reward-curve" }
pallet-timestamp = { version = "2.0.0-dev", path = "../timestamp" }
[features]
default = ["std"]
std = [
"serde",
"codec/std",
"sp-application-crypto/std",
"sp-core/std",
"sp-finality-grandpa/std",
"sp-session/std",
"sp-std/std",
"frame-support/std",
"sp-runtime/std",
+406
View File
@@ -0,0 +1,406 @@
// Copyright 2017-2020 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/>.
//!
//! An opt-in utility module for reporting equivocations.
//!
//! This module defines an offence type for GRANDPA equivocations
//! and some utility traits to wire together:
//! - a key ownership proof system (e.g. to prove that a given authority was
//! part of a session);
//! - a system for reporting offences;
//! - a system for signing and submitting transactions;
//!
//! These can be used in an offchain context in order to submit equivocation
//! reporting extrinsics (from the client that's running the GRANDPA protocol).
//! And in a runtime context, so that the GRANDPA module can validate the
//! equivocation proofs in the extrinsic and report the offences.
//!
use sp_std::prelude::*;
use codec::{self as codec, Decode, Encode};
use frame_support::{debug, dispatch::IsSubType, traits::KeyOwnerProofSystem};
use frame_system::offchain::{AppCrypto, CreateSignedTransaction, Signer};
use sp_finality_grandpa::{EquivocationProof, RoundNumber, SetId};
use sp_runtime::{
traits::{DispatchInfoOf, SignedExtension},
transaction_validity::{
InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,
},
DispatchResult, Perbill,
};
use sp_staking::{
offence::{Kind, Offence, OffenceError, ReportOffence},
SessionIndex,
};
/// Ensure that equivocation reports are only processed if valid.
#[derive(Encode, Decode, Clone, Eq, PartialEq)]
pub struct ValidateEquivocationReport<T>(sp_std::marker::PhantomData<T>);
impl<T> Default for ValidateEquivocationReport<T> {
fn default() -> ValidateEquivocationReport<T> {
ValidateEquivocationReport::new()
}
}
impl<T> ValidateEquivocationReport<T> {
pub fn new() -> ValidateEquivocationReport<T> {
ValidateEquivocationReport(Default::default())
}
}
impl<T> sp_std::fmt::Debug for ValidateEquivocationReport<T> {
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "ValidateEquivocationReport<T>")
}
}
/// Custom validity error used when validating equivocation reports.
#[derive(Debug)]
#[repr(u8)]
pub enum ReportEquivocationValidityError {
/// The proof provided in the report is not valid.
InvalidEquivocationProof = 1,
/// The proof provided in the report is not valid.
InvalidKeyOwnershipProof = 2,
/// The set id provided in the report is not valid.
InvalidSetId = 3,
/// The session index provided in the report is not valid.
InvalidSession = 4,
}
impl From<ReportEquivocationValidityError> for TransactionValidityError {
fn from(e: ReportEquivocationValidityError) -> TransactionValidityError {
TransactionValidityError::from(InvalidTransaction::Custom(e as u8))
}
}
impl<T: super::Trait + Send + Sync> SignedExtension for ValidateEquivocationReport<T>
where
<T as frame_system::Trait>::Call: IsSubType<super::Module<T>, T>,
{
const IDENTIFIER: &'static str = "ValidateEquivocationReport";
type AccountId = T::AccountId;
type Call = <T as frame_system::Trait>::Call;
type AdditionalSigned = ();
type Pre = ();
fn additional_signed(
&self,
) -> sp_std::result::Result<Self::AdditionalSigned, TransactionValidityError> {
Ok(())
}
fn validate(
&self,
_who: &Self::AccountId,
call: &Self::Call,
_info: &DispatchInfoOf<Self::Call>,
_len: usize,
) -> TransactionValidity {
let (equivocation_proof, key_owner_proof) = match call.is_sub_type() {
Some(super::Call::report_equivocation(equivocation_proof, key_owner_proof)) => {
(equivocation_proof, key_owner_proof)
}
_ => return Ok(ValidTransaction::default()),
};
// validate the key ownership proof extracting the id of the offender.
if let None = T::KeyOwnerProofSystem::check_proof(
(
sp_finality_grandpa::KEY_TYPE,
equivocation_proof.offender().clone(),
),
key_owner_proof.clone(),
) {
return Err(ReportEquivocationValidityError::InvalidKeyOwnershipProof.into());
}
// we check the equivocation within the context of its set id (and
// associated session).
let set_id = equivocation_proof.set_id();
let session_index = key_owner_proof.session();
// validate equivocation proof (check votes are different and
// signatures are valid).
if let Err(_) = sp_finality_grandpa::check_equivocation_proof(equivocation_proof.clone()) {
return Err(ReportEquivocationValidityError::InvalidEquivocationProof.into());
}
// fetch the current and previous sets last session index. on the
// genesis set there's no previous set.
let previous_set_id_session_index = if set_id == 0 {
None
} else {
let session_index =
if let Some(session_id) = <super::Module<T>>::session_for_set(set_id - 1) {
session_id
} else {
return Err(ReportEquivocationValidityError::InvalidSetId.into());
};
Some(session_index)
};
let set_id_session_index =
if let Some(session_id) = <super::Module<T>>::session_for_set(set_id) {
session_id
} else {
return Err(ReportEquivocationValidityError::InvalidSetId.into());
};
// check that the session id for the membership proof is within the
// bounds of the set id reported in the equivocation.
if session_index > set_id_session_index ||
previous_set_id_session_index
.map(|previous_index| session_index <= previous_index)
.unwrap_or(false)
{
return Err(ReportEquivocationValidityError::InvalidSession.into());
}
Ok(ValidTransaction::default())
}
}
/// A trait with utility methods for handling equivocation reports in GRANDPA.
/// The offence type is generic, and the trait provides , reporting an offence
/// triggered by a valid equivocation report, and also for creating and
/// submitting equivocation report extrinsics (useful only in offchain context).
pub trait HandleEquivocation<T: super::Trait> {
/// The offence type used for reporting offences on valid equivocation reports.
type Offence: GrandpaOffence<T::KeyOwnerIdentification>;
/// Report an offence proved by the given reporters.
fn report_offence(
reporters: Vec<T::AccountId>,
offence: Self::Offence,
) -> Result<(), OffenceError>;
/// Create and dispatch an equivocation report extrinsic.
fn submit_equivocation_report(
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult;
}
impl<T: super::Trait> HandleEquivocation<T> for () {
type Offence = GrandpaEquivocationOffence<T::KeyOwnerIdentification>;
fn report_offence(
_reporters: Vec<T::AccountId>,
_offence: GrandpaEquivocationOffence<T::KeyOwnerIdentification>,
) -> Result<(), OffenceError> {
Ok(())
}
fn submit_equivocation_report(
_equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
_key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult {
Ok(())
}
}
/// Generic equivocation handler. This type implements `HandleEquivocation`
/// using existing subsystems that are part of frame (type bounds described
/// below) and will dispatch to them directly, it's only purpose is to wire all
/// subsystems together.
pub struct EquivocationHandler<I, C, S, R, O = GrandpaEquivocationOffence<I>> {
_phantom: sp_std::marker::PhantomData<(I, C, S, R, O)>,
}
impl<I, C, S, R, O> Default for EquivocationHandler<I, C, S, R, O> {
fn default() -> Self {
Self {
_phantom: Default::default(),
}
}
}
impl<T, C, S, R, O> HandleEquivocation<T>
for EquivocationHandler<T::KeyOwnerIdentification, C, S, R, O>
where
// A signed transaction creator. Used for signing and submitting equivocation reports.
T: super::Trait + CreateSignedTransaction<super::Call<T>>,
// Application-specific crypto bindings.
C: AppCrypto<T::Public, T::Signature>,
// The offence type that should be used when reporting.
O: GrandpaOffence<T::KeyOwnerIdentification>,
// A system for reporting offences after valid equivocation reports are
// processed.
R: ReportOffence<T::AccountId, T::KeyOwnerIdentification, O>,
{
type Offence = O;
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
R::report_offence(reporters, offence)
}
fn submit_equivocation_report(
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult {
use frame_system::offchain::SendSignedTransaction;
let signer = Signer::<T, C>::all_accounts();
if !signer.can_sign() {
return Err(
"No local accounts available. Consider adding one via `author_insertKey` RPC.",
)?;
}
let results = signer.send_signed_transaction(|_account| {
super::Call::report_equivocation(equivocation_proof.clone(), key_owner_proof.clone())
});
for (acc, res) in &results {
match res {
Ok(()) => debug::info!("[{:?}] Submitted GRANDPA equivocation report.", acc.id),
Err(e) => debug::error!(
"[{:?}] Error submitting equivocation report: {:?}",
acc.id,
e
),
}
}
Ok(())
}
}
/// A round number and set id which point on the time of an offence.
#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)]
pub struct GrandpaTimeSlot {
// The order of these matters for `derive(Ord)`.
/// Grandpa Set ID.
pub set_id: SetId,
/// Round number.
pub round: RoundNumber,
}
/// A grandpa equivocation offence report.
#[allow(dead_code)]
pub struct GrandpaEquivocationOffence<FullIdentification> {
/// Time slot at which this incident happened.
pub time_slot: GrandpaTimeSlot,
/// The session index in which the incident happened.
pub session_index: SessionIndex,
/// The size of the validator set at the time of the offence.
pub validator_set_count: u32,
/// The authority which produced this equivocation.
pub offender: FullIdentification,
}
/// An interface for types that will be used as GRANDPA offences and must also
/// implement the `Offence` trait. This trait provides a constructor that is
/// provided all available data during processing of GRANDPA equivocations.
pub trait GrandpaOffence<FullIdentification>: Offence<FullIdentification> {
/// Create a new GRANDPA offence using the given equivocation details.
fn new(
session_index: SessionIndex,
validator_set_count: u32,
offender: FullIdentification,
set_id: SetId,
round: RoundNumber,
) -> Self;
}
impl<FullIdentification: Clone> GrandpaOffence<FullIdentification>
for GrandpaEquivocationOffence<FullIdentification>
{
fn new(
session_index: SessionIndex,
validator_set_count: u32,
offender: FullIdentification,
set_id: SetId,
round: RoundNumber,
) -> Self {
GrandpaEquivocationOffence {
session_index,
validator_set_count,
offender,
time_slot: GrandpaTimeSlot { set_id, round },
}
}
}
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()
}
}
/// A trait to get a session number the `MembershipProof` belongs to.
pub trait GetSessionNumber {
fn session(&self) -> SessionIndex;
}
/// A trait to get the validator count at the session the `MembershipProof`
/// belongs to.
pub trait GetValidatorCount {
fn validator_count(&self) -> sp_session::ValidatorCount;
}
impl GetSessionNumber for frame_support::Void {
fn session(&self) -> SessionIndex {
Default::default()
}
}
impl GetValidatorCount for frame_support::Void {
fn validator_count(&self) -> sp_session::ValidatorCount {
Default::default()
}
}
impl GetSessionNumber for sp_session::MembershipProof {
fn session(&self) -> SessionIndex {
self.session()
}
}
impl GetValidatorCount for sp_session::MembershipProof {
fn validator_count(&self) -> sp_session::ValidatorCount {
self.validator_count()
}
}
+120 -74
View File
@@ -31,27 +31,65 @@
pub use sp_finality_grandpa as fg_primitives;
use sp_std::prelude::*;
use codec::{self as codec, Encode, Decode};
use frame_support::{decl_event, decl_storage, decl_module, decl_error, storage};
use sp_runtime::{
DispatchResult, generic::{DigestItem, OpaqueDigestItemId}, traits::Zero, Perbill,
};
use sp_staking::{
SessionIndex,
offence::{Offence, Kind},
};
use fg_primitives::{
GRANDPA_AUTHORITIES_KEY, GRANDPA_ENGINE_ID, ScheduledChange, ConsensusLog, SetId, RoundNumber,
};
pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList};
use frame_system::{self as system, ensure_signed, DigestOf};
use codec::{self as codec, Decode, Encode};
pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList};
use fg_primitives::{
ConsensusLog, EquivocationProof, ScheduledChange, SetId, GRANDPA_AUTHORITIES_KEY,
GRANDPA_ENGINE_ID,
};
use frame_support::{
decl_error, decl_event, decl_module, decl_storage, storage, traits::KeyOwnerProofSystem,
Parameter,
};
use frame_system::{self as system, ensure_signed, DigestOf};
use sp_runtime::{
generic::{DigestItem, OpaqueDigestItemId},
traits::Zero,
DispatchResult, KeyTypeId,
};
use sp_staking::SessionIndex;
mod equivocation;
mod mock;
mod tests;
pub use equivocation::{
EquivocationHandler, GetSessionNumber, GetValidatorCount, GrandpaEquivocationOffence,
GrandpaOffence, GrandpaTimeSlot, HandleEquivocation, ValidateEquivocationReport,
};
pub trait Trait: frame_system::Trait {
/// The event type of this module.
type Event: From<Event> + Into<<Self as frame_system::Trait>::Event>;
/// The function call.
type Call: From<Call<Self>>;
/// The proof of key ownership, used for validating equivocation reports.
/// The proof must include the session index and validator count of the
/// session at which the equivocation occurred.
type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount;
/// The identification of a key owner, used when reporting equivocations.
type KeyOwnerIdentification: Parameter;
/// A system for proving ownership of keys, i.e. that a given key was part
/// of a validator set, needed for validating equivocation reports.
type KeyOwnerProofSystem: KeyOwnerProofSystem<
(KeyTypeId, AuthorityId),
Proof = Self::KeyOwnerProof,
IdentificationTuple = Self::KeyOwnerIdentification,
>;
/// The equivocation handling subsystem, defines methods to report an
/// offence (after the equivocation has been validated) and for submitting a
/// transaction to report an equivocation (from an offchain context).
/// NOTE: when enabling equivocation handling (i.e. this type isn't set to
/// `()`) you must add the `equivocation::ValidateEquivocationReport` signed
/// extension to the runtime's `SignedExtra` definition, otherwise
/// equivocation reports won't be properly validated.
type HandleEquivocation: HandleEquivocation<Self>;
}
/// A stored pending change, old format.
@@ -173,7 +211,9 @@ decl_storage! {
}
add_extra_genesis {
config(authorities): AuthorityList;
build(|config| Module::<T>::initialize_authorities(&config.authorities))
build(|config| {
Module::<T>::initialize(&config.authorities)
})
}
}
@@ -183,11 +223,49 @@ decl_module! {
fn deposit_event() = default;
/// Report some misbehavior.
/// Report voter equivocation/misbehavior. This method will verify the
/// equivocation proof and validate the given key ownership proof
/// against the extracted offender. If both are valid, the offence
/// will be reported.
///
/// Since the weight is 0 in order to avoid DoS pre-validation is implemented in a
/// `SignedExtension`.
#[weight = 0]
fn report_misbehavior(origin, _report: Vec<u8>) {
ensure_signed(origin)?;
// FIXME: https://github.com/paritytech/substrate/issues/1112
fn report_equivocation(
origin,
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
key_owner_proof: T::KeyOwnerProof,
) {
let reporter_id = ensure_signed(origin)?;
let (session_index, validator_set_count) = (
key_owner_proof.session(),
key_owner_proof.validator_count(),
);
// we have already checked this proof in `SignedExtension`, we to
// check it again to get the full identification of the offender.
let offender =
T::KeyOwnerProofSystem::check_proof(
(fg_primitives::KEY_TYPE, equivocation_proof.offender().clone()),
key_owner_proof,
).ok_or("Invalid key ownership proof.")?;
// the set id and round when the offence happened
let set_id = equivocation_proof.set_id();
let round = equivocation_proof.round();
// report to the offences module rewarding the sender.
T::HandleEquivocation::report_offence(
vec![reporter_id],
<T::HandleEquivocation as HandleEquivocation<T>>::Offence::new(
session_index,
validator_set_count,
offender,
set_id,
round,
),
).map_err(|_| "Duplicate offence report.")?;
}
fn on_finalize(block_number: T::BlockNumber) {
@@ -351,7 +429,9 @@ impl<T: Trait> Module<T> {
<frame_system::Module<T>>::deposit_log(log.into());
}
fn initialize_authorities(authorities: &AuthorityList) {
// Perform module initialization, abstracted so that it can be called either through genesis
// config builder or through `on_genesis_session`.
fn initialize(authorities: &AuthorityList) {
if !authorities.is_empty() {
assert!(
Self::grandpa_authorities().is_empty(),
@@ -359,6 +439,25 @@ impl<T: Trait> Module<T> {
);
Self::set_grandpa_authorities(authorities);
}
// NOTE: initialize first session of first set. this is necessary
// because we only update this `on_new_session` which isn't called
// for the genesis session.
SetIdSession::insert(0, 0);
}
/// Submits an extrinsic to report an equivocation. This method will sign an
/// extrinsic with a call to `report_equivocation` with any reporting keys
/// available in the keystore and will push the transaction to the pool.
/// Only useful in an offchain context.
pub fn submit_report_equivocation_extrinsic(
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
key_owner_proof: T::KeyOwnerProof,
) -> Option<()> {
T::HandleEquivocation::submit_equivocation_report(equivocation_proof, key_owner_proof)
.ok()?;
Some(())
}
}
@@ -411,7 +510,7 @@ impl<T: Trait> pallet_session::OneSessionHandler<T::AccountId> for Module<T>
where I: Iterator<Item=(&'a T::AccountId, AuthorityId)>
{
let authorities = validators.map(|(_, k)| (k, 1)).collect::<Vec<_>>();
Self::initialize_authorities(&authorities);
Self::initialize(&authorities);
}
fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I)
@@ -453,56 +552,3 @@ impl<T: Trait> pallet_finality_tracker::OnFinalizationStalled<T::BlockNumber> fo
<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)]
pub struct GrandpaTimeSlot {
// The order of these matters for `derive(Ord)`.
/// Grandpa Set ID.
pub set_id: SetId,
/// Round number.
pub round: RoundNumber,
}
/// A grandpa equivocation offence report.
pub struct GrandpaEquivocationOffence<FullIdentification> {
/// Time slot at which this incident happened.
pub time_slot: GrandpaTimeSlot,
/// The session index in which the incident happened.
pub session_index: SessionIndex,
/// The size of the validator set at the time of the offence.
pub validator_set_count: u32,
/// The authority which produced this equivocation.
pub 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()
}
}
+424 -29
View File
@@ -18,41 +18,85 @@
#![cfg(test)]
use sp_runtime::{Perbill, DigestItem, traits::IdentityLookup, testing::{Header, UintAuthorityId}};
use crate::{
equivocation::ValidateEquivocationReport, AuthorityId, AuthorityList, Call as GrandpaCall,
ConsensusLog, Module, Trait,
};
use ::grandpa as finality_grandpa;
use codec::Encode;
use frame_support::{
impl_outer_dispatch, impl_outer_event, impl_outer_origin, parameter_types,
traits::{KeyOwnerProofSystem, OnFinalize, OnInitialize},
weights::{DispatchInfo, Weight},
};
use pallet_staking::EraIndex;
use sp_core::{crypto::KeyTypeId, H256};
use sp_finality_grandpa::{RoundNumber, SetId, GRANDPA_ENGINE_ID};
use sp_io;
use frame_support::{impl_outer_origin, impl_outer_event, parameter_types, weights::Weight};
use sp_core::H256;
use codec::{Encode, Decode};
use crate::{AuthorityId, AuthorityList, GenesisConfig, Trait, Module, ConsensusLog};
use sp_finality_grandpa::GRANDPA_ENGINE_ID;
use sp_keyring::Ed25519Keyring;
use sp_runtime::{
curve::PiecewiseLinear,
impl_opaque_keys,
testing::{Header, TestXt, UintAuthorityId},
traits::{
Convert, Extrinsic as ExtrinsicT, Header as _, IdentityLookup, OpaqueKeys,
SaturatedConversion, SignedExtension,
},
transaction_validity::TransactionValidityError,
DigestItem, Perbill,
};
use sp_staking::SessionIndex;
use frame_system as system;
impl_outer_origin!{
pub enum Origin for Test where system = frame_system {}
use pallet_balances as balances;
use pallet_offences as offences;
use pallet_session as session;
use pallet_staking as staking;
use pallet_timestamp as timestamp;
impl_outer_origin! {
pub enum Origin for Test {}
}
pub fn grandpa_log(log: ConsensusLog<u64>) -> DigestItem<H256> {
DigestItem::Consensus(GRANDPA_ENGINE_ID, log.encode())
impl_outer_dispatch! {
pub enum Call for Test where origin: Origin {
grandpa::Grandpa,
staking::Staking,
}
}
// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
#[derive(Clone, PartialEq, Eq, Debug, Decode, Encode)]
impl_opaque_keys! {
pub struct TestSessionKeys {
pub grandpa_authority: super::Module<Test>,
}
}
impl_outer_event! {
pub enum TestEvent for Test {
system<T>,
balances<T>,
grandpa,
offences,
session,
staking<T>,
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Test;
impl Trait for Test {
type Event = TestEvent;
}
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const MaximumBlockWeight: Weight = 1024;
pub const MaximumBlockLength: u32 = 2 * 1024;
pub const AvailableBlockRatio: Perbill = Perbill::one();
}
impl frame_system::Trait for Test {
type Origin = Origin;
type Index = u64;
type BlockNumber = u64;
type Call = ();
type Call = Call;
type Hash = H256;
type Hashing = sp_runtime::traits::BlakeTwo256;
type AccountId = u64;
@@ -68,20 +112,220 @@ impl frame_system::Trait for Test {
type AvailableBlockRatio = AvailableBlockRatio;
type Version = ();
type ModuleToIndex = ();
type AccountData = ();
type AccountData = balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
}
impl<C> system::offchain::SendTransactionTypes<C> for Test
where
Call: From<C>,
{
type OverarchingCall = Call;
type Extrinsic = TestXt<Call, ()>;
}
parameter_types! {
pub const Period: u64 = 1;
pub const Offset: u64 = 0;
pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(17);
}
/// Custom `SessionHandler` since we use `TestSessionKeys` as `Keys`.
impl session::Trait for Test {
type Event = TestEvent;
type ValidatorId = u64;
type ValidatorIdOf = staking::StashOf<Self>;
type ShouldEndSession = session::PeriodicSessions<Period, Offset>;
type NextSessionRotation = session::PeriodicSessions<Period, Offset>;
type SessionManager = session::historical::NoteHistoricalRoot<Self, Staking>;
type SessionHandler = <TestSessionKeys as OpaqueKeys>::KeyTypeIdProviders;
type Keys = TestSessionKeys;
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
}
impl session::historical::Trait for Test {
type FullIdentification = staking::Exposure<u64, u128>;
type FullIdentificationOf = staking::ExposureOf<Self>;
}
parameter_types! {
pub const ExistentialDeposit: u128 = 1;
}
impl balances::Trait for Test {
type Balance = u128;
type DustRemoval = ();
type Event = TestEvent;
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
}
parameter_types! {
pub const MinimumPeriod: u64 = 3;
}
impl timestamp::Trait for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = MinimumPeriod;
}
pallet_staking_reward_curve::build! {
const REWARD_CURVE: PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000u64,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
parameter_types! {
pub const SessionsPerEra: SessionIndex = 3;
pub const BondingDuration: EraIndex = 3;
pub const SlashDeferDuration: EraIndex = 0;
pub const AttestationPeriod: u64 = 100;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const MaxNominatorRewardedPerValidator: u32 = 64;
pub const ElectionLookahead: u64 = 0;
pub const StakingUnsignedPriority: u64 = u64::max_value() / 2;
}
pub struct CurrencyToVoteHandler;
impl Convert<u128, u128> for CurrencyToVoteHandler {
fn convert(x: u128) -> u128 {
x
}
}
impl Convert<u128, u64> for CurrencyToVoteHandler {
fn convert(x: u128) -> u64 {
x.saturated_into()
}
}
impl staking::Trait for Test {
type RewardRemainder = ();
type CurrencyToVote = CurrencyToVoteHandler;
type Event = TestEvent;
type Currency = Balances;
type Slash = ();
type Reward = ();
type SessionsPerEra = SessionsPerEra;
type BondingDuration = BondingDuration;
type SlashDeferDuration = SlashDeferDuration;
type SlashCancelOrigin = system::EnsureRoot<Self::AccountId>;
type SessionInterface = Self;
type UnixTime = timestamp::Module<Test>;
type RewardCurve = RewardCurve;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type NextNewSession = Session;
type ElectionLookahead = ElectionLookahead;
type Call = Call;
type UnsignedPriority = StakingUnsignedPriority;
type MaxIterations = ();
}
impl offences::Trait for Test {
type Event = TestEvent;
type IdentificationTuple = session::historical::IdentificationTuple<Self>;
type OnOffenceHandler = Staking;
}
impl Trait for Test {
type Event = TestEvent;
type Call = Call;
type KeyOwnerProofSystem = Historical;
type KeyOwnerProof =
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, AuthorityId)>>::Proof;
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
KeyTypeId,
AuthorityId,
)>>::IdentificationTuple;
type HandleEquivocation = super::EquivocationHandler<
Self::KeyOwnerIdentification,
reporting_keys::ReporterAppCrypto,
Test,
Offences,
>;
}
pub mod reporting_keys {
use sp_core::crypto::KeyTypeId;
pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test");
mod app {
use sp_application_crypto::{app_crypto, ed25519};
app_crypto!(ed25519, super::KEY_TYPE);
impl sp_runtime::traits::IdentifyAccount for Public {
type AccountId = u64;
fn into_account(self) -> Self::AccountId {
super::super::Grandpa::grandpa_authorities()
.iter()
.map(|(k, _)| k)
.position(|b| *b == self.0.clone().into())
.unwrap() as u64
}
}
}
pub type ReporterId = app::Public;
pub struct ReporterAppCrypto;
impl frame_system::offchain::AppCrypto<ReporterId, sp_core::ed25519::Signature>
for ReporterAppCrypto
{
type RuntimeAppPublic = ReporterId;
type GenericSignature = sp_core::ed25519::Signature;
type GenericPublic = sp_core::ed25519::Public;
}
}
type Extrinsic = TestXt<Call, ()>;
impl<LocalCall> system::offchain::CreateSignedTransaction<LocalCall> for Test
where
Call: From<LocalCall>,
{
fn create_transaction<C: system::offchain::AppCrypto<Self::Public, Self::Signature>>(
call: Call,
_public: reporting_keys::ReporterId,
_account: <Test as system::Trait>::AccountId,
nonce: <Test as system::Trait>::Index,
) -> Option<(Call, <Extrinsic as ExtrinsicT>::SignaturePayload)> {
Some((call, (nonce, ())))
}
}
impl frame_system::offchain::SigningTypes for Test {
type Public = reporting_keys::ReporterId;
type Signature = sp_core::ed25519::Signature;
}
mod grandpa {
pub use crate::Event;
}
impl_outer_event!{
pub enum TestEvent for Test {
system<T>,
grandpa,
}
pub type Balances = pallet_balances::Module<Test>;
pub type Historical = pallet_session::historical::Module<Test>;
pub type Offences = pallet_offences::Module<Test>;
pub type Session = pallet_session::Module<Test>;
pub type Staking = pallet_staking::Module<Test>;
pub type System = frame_system::Module<Test>;
pub type Timestamp = pallet_timestamp::Module<Test>;
pub type Grandpa = Module<Test>;
pub fn grandpa_log(log: ConsensusLog<u64>) -> DigestItem<H256> {
DigestItem::Consensus(GRANDPA_ENGINE_ID, log.encode())
}
pub fn to_authorities(vec: Vec<(u64, u64)>) -> AuthorityList {
@@ -90,13 +334,164 @@ pub fn to_authorities(vec: Vec<(u64, u64)>) -> AuthorityList {
.collect()
}
pub fn new_test_ext(authorities: Vec<(u64, u64)>) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
GenesisConfig {
authorities: to_authorities(authorities),
}.assimilate_storage::<Test>(&mut t).unwrap();
pub fn extract_keyring(id: &AuthorityId) -> Ed25519Keyring {
let mut raw_public = [0; 32];
raw_public.copy_from_slice(id.as_ref());
Ed25519Keyring::from_raw_public(raw_public).unwrap()
}
pub fn new_test_ext(vec: Vec<(u64, u64)>) -> sp_io::TestExternalities {
new_test_ext_raw_authorities(to_authorities(vec))
}
pub fn new_test_ext_raw_authorities(authorities: AuthorityList) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default()
.build_storage::<Test>()
.unwrap();
// stashes are the index.
let session_keys: Vec<_> = authorities
.iter()
.enumerate()
.map(|(i, (k, _))| {
(
i as u64,
i as u64,
TestSessionKeys {
grandpa_authority: AuthorityId::from(k.clone()),
},
)
})
.collect();
// controllers are the index + 1000
let stakers: Vec<_> = (0..authorities.len())
.map(|i| {
(
i as u64,
i as u64 + 1000,
10_000,
staking::StakerStatus::<u64>::Validator,
)
})
.collect();
let balances: Vec<_> = (0..authorities.len())
.map(|i| (i as u64, 10_000_000))
.collect();
// NOTE: this will initialize the grandpa authorities
// through OneSessionHandler::on_genesis_session
session::GenesisConfig::<Test> { keys: session_keys }
.assimilate_storage(&mut t)
.unwrap();
balances::GenesisConfig::<Test> { balances }
.assimilate_storage(&mut t)
.unwrap();
let staking_config = staking::GenesisConfig::<Test> {
stakers,
validator_count: 8,
force_era: staking::Forcing::ForceNew,
minimum_validator_count: 0,
invulnerables: vec![],
..Default::default()
};
staking_config.assimilate_storage(&mut t).unwrap();
t.into()
}
pub type System = frame_system::Module<Test>;
pub type Grandpa = Module<Test>;
pub fn start_session(session_index: SessionIndex) {
let mut parent_hash = System::parent_hash();
for i in Session::current_index()..session_index {
Staking::on_finalize(System::block_number());
System::set_block_number((i + 1).into());
Timestamp::set_timestamp(System::block_number() * 6000);
// In order to be able to use `System::parent_hash()` in the tests
// we need to first get it via `System::finalize` and then set it
// the `System::initialize`. However, it is needed to be taken into
// consideration that finalizing will prune some data in `System`
// storage including old values `BlockHash` if that reaches above
// `BlockHashCount` capacity.
if System::block_number() > 1 {
let hdr = System::finalize();
parent_hash = hdr.hash();
}
System::initialize(
&(i as u64 + 1),
&parent_hash,
&Default::default(),
&Default::default(),
Default::default(),
);
Session::on_initialize(System::block_number());
System::on_initialize(System::block_number());
}
assert_eq!(Session::current_index(), session_index);
}
pub fn start_era(era_index: EraIndex) {
start_session((era_index * 3).into());
assert_eq!(Staking::current_era(), Some(era_index));
}
pub fn initialize_block(number: u64, parent_hash: H256) {
System::initialize(
&number,
&parent_hash,
&Default::default(),
&Default::default(),
Default::default(),
);
}
pub fn report_equivocation(
equivocation_proof: sp_finality_grandpa::EquivocationProof<H256, u64>,
key_owner_proof: sp_session::MembershipProof,
) -> Result<GrandpaCall<Test>, TransactionValidityError> {
let inner = GrandpaCall::report_equivocation(equivocation_proof, key_owner_proof);
let call = Call::Grandpa(inner.clone());
ValidateEquivocationReport::<Test>::new().validate(&0, &call, &DispatchInfo::default(), 0)?;
Ok(inner)
}
pub fn generate_equivocation_proof(
set_id: SetId,
vote1: (RoundNumber, H256, u64, &Ed25519Keyring),
vote2: (RoundNumber, H256, u64, &Ed25519Keyring),
) -> sp_finality_grandpa::EquivocationProof<H256, u64> {
let signed_prevote = |round, hash, number, keyring: &Ed25519Keyring| {
let prevote = finality_grandpa::Prevote {
target_hash: hash,
target_number: number,
};
let prevote_msg = finality_grandpa::Message::Prevote(prevote.clone());
let payload = sp_finality_grandpa::localized_payload(round, set_id, &prevote_msg);
let signed = keyring.sign(&payload).into();
(prevote, signed)
};
let (prevote1, signed1) = signed_prevote(vote1.0, vote1.1, vote1.2, vote1.3);
let (prevote2, signed2) = signed_prevote(vote2.0, vote2.1, vote2.2, vote2.3);
sp_finality_grandpa::EquivocationProof::new(
set_id,
sp_finality_grandpa::Equivocation::Prevote(finality_grandpa::Equivocation {
round_number: vote1.0,
identity: vote1.3.public().into(),
first: (prevote1, signed1),
second: (prevote2, signed2),
}),
)
}
+354 -14
View File
@@ -18,23 +18,18 @@
#![cfg(test)]
use sp_runtime::{testing::{H256, Digest}, traits::Header};
use frame_support::traits::OnFinalize;
use super::*;
use crate::mock::*;
use frame_system::{EventRecord, Phase};
use codec::{Decode, Encode};
use fg_primitives::ScheduledChange;
use super::*;
fn initialize_block(number: u64, parent_hash: H256) {
System::initialize(
&number,
&parent_hash,
&Default::default(),
&Default::default(),
Default::default(),
);
}
use frame_support::{
assert_err, assert_ok,
traits::{Currency, OnFinalize},
};
use frame_system::{EventRecord, Phase};
use sp_core::H256;
use sp_keyring::Ed25519Keyring;
use sp_runtime::{testing::Digest, traits::Header};
#[test]
fn authorities_change_logged() {
@@ -319,3 +314,348 @@ fn time_slot_have_sane_ord() {
];
assert!(FIXTURE.windows(2).all(|f| f[0] < f[1]));
}
fn test_authorities() -> AuthorityList {
let authorities = vec![
Ed25519Keyring::Alice,
Ed25519Keyring::Bob,
Ed25519Keyring::Charlie,
];
authorities
.into_iter()
.map(|id| (id.public().into(), 1u64))
.collect()
}
#[test]
fn report_equivocation_current_set_works() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
assert_eq!(Staking::current_era(), Some(0));
assert_eq!(Session::current_index(), 0);
start_era(1);
let authorities = Grandpa::grandpa_authorities();
// make sure that all authorities have the same balance
for i in 0..authorities.len() {
assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000);
assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000);
assert_eq!(
Staking::eras_stakers(1, i as u64),
pallet_staking::Exposure {
total: 10_000,
own: 10_000,
others: vec![],
},
);
}
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index].0;
let equivocation_keyring = extract_keyring(equivocation_key);
let set_id = Grandpa::current_set_id();
// generate an equivocation proof, with two votes in the same round for
// different block hashes signed by the same key
let equivocation_proof = generate_equivocation_proof(
set_id,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &equivocation_keyring),
);
// create the key ownership proof
let key_owner_proof =
Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap();
// report the equivocation and the tx should be dispatched successfully
let inner = report_equivocation(equivocation_proof, key_owner_proof).unwrap();
assert_ok!(Grandpa::dispatch(inner, Origin::signed(1)));
start_era(2);
// check that the balance of 0-th validator is slashed 100%.
assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000);
assert_eq!(Staking::slashable_balance_of(&0), 0);
assert_eq!(
Staking::eras_stakers(2, 0),
pallet_staking::Exposure {
total: 0,
own: 0,
others: vec![],
},
);
// check that the balances of all other validators are left intact.
for i in 1..authorities.len() {
assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000);
assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000);
assert_eq!(
Staking::eras_stakers(2, i as u64),
pallet_staking::Exposure {
total: 10_000,
own: 10_000,
others: vec![],
},
);
}
});
}
#[test]
fn report_equivocation_old_set_works() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let authorities = Grandpa::grandpa_authorities();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index].0;
// create the key ownership proof in the "old" set
let key_owner_proof =
Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap();
start_era(2);
// make sure that all authorities have the same balance
for i in 0..authorities.len() {
assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000);
assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000);
assert_eq!(
Staking::eras_stakers(2, i as u64),
pallet_staking::Exposure {
total: 10_000,
own: 10_000,
others: vec![],
},
);
}
let equivocation_keyring = extract_keyring(equivocation_key);
let set_id = Grandpa::current_set_id();
// generate an equivocation proof for the old set,
let equivocation_proof = generate_equivocation_proof(
set_id - 1,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &equivocation_keyring),
);
// report the equivocation using the key ownership proof generated on
// the old set, the tx should be dispatched successfully
let inner = report_equivocation(equivocation_proof, key_owner_proof).unwrap();
assert_ok!(Grandpa::dispatch(inner, Origin::signed(1)));
start_era(3);
// check that the balance of 0-th validator is slashed 100%.
assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000);
assert_eq!(Staking::slashable_balance_of(&0), 0);
assert_eq!(
Staking::eras_stakers(3, 0),
pallet_staking::Exposure {
total: 0,
own: 0,
others: vec![],
},
);
// check that the balances of all other validators are left intact.
for i in 1..authorities.len() {
assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000);
assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000);
assert_eq!(
Staking::eras_stakers(3, i as u64),
pallet_staking::Exposure {
total: 10_000,
own: 10_000,
others: vec![],
},
);
}
});
}
#[test]
fn report_equivocation_invalid_set_id() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let authorities = Grandpa::grandpa_authorities();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index].0;
let equivocation_keyring = extract_keyring(equivocation_key);
let key_owner_proof =
Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap();
let set_id = Grandpa::current_set_id();
// generate an equivocation for a future set
let equivocation_proof = generate_equivocation_proof(
set_id + 1,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &equivocation_keyring),
);
// it should be filtered by the signed extension validation
assert_err!(
report_equivocation(equivocation_proof, key_owner_proof),
equivocation::ReportEquivocationValidityError::InvalidSetId,
);
});
}
#[test]
fn report_equivocation_invalid_session() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let authorities = Grandpa::grandpa_authorities();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index].0;
let equivocation_keyring = extract_keyring(equivocation_key);
// generate a key ownership proof at set id = 1
let key_owner_proof =
Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap();
start_era(2);
let set_id = Grandpa::current_set_id();
// generate an equivocation proof at set id = 2
let equivocation_proof = generate_equivocation_proof(
set_id,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &equivocation_keyring),
);
// report an equivocation for the current set using an key ownership
// proof from the previous set, the session should be invalid.
assert_err!(
report_equivocation(equivocation_proof, key_owner_proof),
equivocation::ReportEquivocationValidityError::InvalidSession,
);
});
}
#[test]
fn report_equivocation_invalid_key_owner_proof() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let authorities = Grandpa::grandpa_authorities();
let invalid_owner_authority_index = 1;
let invalid_owner_key = &authorities[invalid_owner_authority_index].0;
// generate a key ownership proof for the authority at index 1
let invalid_key_owner_proof =
Historical::prove((sp_finality_grandpa::KEY_TYPE, &invalid_owner_key)).unwrap();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index].0;
let equivocation_keyring = extract_keyring(equivocation_key);
let set_id = Grandpa::current_set_id();
// generate an equivocation proof for the authority at index 0
let equivocation_proof = generate_equivocation_proof(
set_id,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &equivocation_keyring),
);
// we need to start a new era otherwise the key ownership proof won't be
// checked since the authorities are part of the current session
start_era(2);
// report an equivocation for the current set using a key ownership
// proof for a different key than the one in the equivocation proof.
assert_err!(
report_equivocation(equivocation_proof, invalid_key_owner_proof),
equivocation::ReportEquivocationValidityError::InvalidKeyOwnershipProof,
);
});
}
#[test]
fn report_equivocation_invalid_equivocation_proof() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let authorities = Grandpa::grandpa_authorities();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index].0;
let equivocation_keyring = extract_keyring(equivocation_key);
// generate a key ownership proof at set id = 1
let key_owner_proof =
Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap();
let set_id = Grandpa::current_set_id();
let assert_invalid_equivocation_proof = |equivocation_proof| {
assert_err!(
report_equivocation(equivocation_proof, key_owner_proof.clone()),
equivocation::ReportEquivocationValidityError::InvalidEquivocationProof,
);
};
start_era(2);
// both votes target the same block number and hash,
// there is no equivocation.
assert_invalid_equivocation_proof(generate_equivocation_proof(
set_id,
(1, H256::zero(), 10, &equivocation_keyring),
(1, H256::zero(), 10, &equivocation_keyring),
));
// votes targetting different rounds, there is no equivocation.
assert_invalid_equivocation_proof(generate_equivocation_proof(
set_id,
(1, H256::random(), 10, &equivocation_keyring),
(2, H256::random(), 10, &equivocation_keyring),
));
// votes signed with different authority keys
assert_invalid_equivocation_proof(generate_equivocation_proof(
set_id,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &Ed25519Keyring::Charlie),
));
// votes signed with a key that isn't part of the authority set
assert_invalid_equivocation_proof(generate_equivocation_proof(
set_id,
(1, H256::random(), 10, &equivocation_keyring),
(1, H256::random(), 10, &Ed25519Keyring::Dave),
));
});
}