BEEFY: implement equivocations detection, reporting and slashing (#13121)

* client/beefy: simplify self_vote logic

* client/beefy: migrate to new state version

* client/beefy: detect equivocated votes

* fix typos

* sp-beefy: add equivocation primitives

* client/beefy: refactor vote processing

* fix version migration for new rounds struct

* client/beefy: track equivocations and create proofs

* client/beefy: adjust tests for new voting logic

* sp-beefy: fix commitment ordering and equality

* client/beefy: simplify handle_vote() a bit

* client/beefy: add simple equivocation test

* client/beefy: submit equivocation proof - WIP

* frame/beefy: add equivocation report runtime api - part 1

* frame/beefy: report equivocation logic - part 2

* frame/beefy: add pluggable Equivocation handler - part 3

* frame/beefy: impl ValidateUnsigned for equivocations reporting

* client/beefy: submit report equivocation unsigned extrinsic

* primitives/beefy: fix tests

* frame/beefy: add default weights

* frame/beefy: fix tests

* client/beefy: fix tests

* frame/beefy-mmr: fix tests

* frame/beefy: cross-check session index with equivocation report

* sp-beefy: make test Keyring useable in pallet

* frame/beefy: add basic equivocation test

* frame/beefy: test verify equivocation results in slashing

* frame/beefy: test report_equivocation_old_set

* frame/beefy: add more equivocation tests

* sp-beefy: fix docs

* beefy: simplify equivocations and fix tests

* client/beefy: address review comments

* frame/beefy: add ValidateUnsigned to test/mock runtime

* client/beefy: fixes after merge master

* fix missed merge damage

* client/beefy: add test for reporting equivocations

Also validated there's no unexpected equivocations reported in the
other tests.

Signed-off-by: acatangiu <adrian@parity.io>

* sp-beefy: move test utils to their own file

* client/beefy: add negative test for equivocation reports

* sp-beefy: move back MmrRootProvider - used in polkadot-service

* impl review suggestions

* client/beefy: add equivocation metrics

---------

Signed-off-by: acatangiu <adrian@parity.io>
Co-authored-by: parity-processbot <>
This commit is contained in:
Adrian Catangiu
2023-02-17 11:45:00 +02:00
committed by GitHub
parent 36480b158d
commit c21f292a02
22 changed files with 2141 additions and 214 deletions
@@ -0,0 +1,54 @@
// This file is part of Substrate.
// Copyright (C) 2020-2023 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Default weights for the BEEFY Pallet
//! This file was not auto-generated.
use frame_support::weights::{
constants::{RocksDbWeight as DbWeight, WEIGHT_REF_TIME_PER_MICROS, WEIGHT_REF_TIME_PER_NANOS},
Weight,
};
impl crate::WeightInfo for () {
fn report_equivocation(validator_count: u32) -> Weight {
// we take the validator set count from the membership proof to
// calculate the weight but we set a floor of 100 validators.
let validator_count = validator_count.max(100) as u64;
// worst case we are considering is that the given offender is backed by 200 nominators
const MAX_NOMINATORS: u64 = 200;
// checking membership proof
Weight::from_ref_time(35u64 * WEIGHT_REF_TIME_PER_MICROS)
.saturating_add(
Weight::from_ref_time(175u64 * WEIGHT_REF_TIME_PER_NANOS)
.saturating_mul(validator_count),
)
.saturating_add(DbWeight::get().reads(5))
// check equivocation proof
.saturating_add(Weight::from_ref_time(95u64 * WEIGHT_REF_TIME_PER_MICROS))
// report offence
.saturating_add(Weight::from_ref_time(110u64 * WEIGHT_REF_TIME_PER_MICROS))
.saturating_add(Weight::from_ref_time(
25u64 * WEIGHT_REF_TIME_PER_MICROS * MAX_NOMINATORS,
))
.saturating_add(DbWeight::get().reads(14 + 3 * MAX_NOMINATORS))
.saturating_add(DbWeight::get().writes(10 + 3 * MAX_NOMINATORS))
// fetching set id -> session index mappings
.saturating_add(DbWeight::get().reads(2))
}
}
+385
View File
@@ -0,0 +1,385 @@
// This file is part of Substrate.
// Copyright (C) 2017-2023 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! An opt-in utility module for reporting equivocations.
//!
//! This module defines an offence type for BEEFY 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;
//! - a way to get the current block author;
//!
//! These can be used in an offchain context in order to submit equivocation
//! reporting extrinsics (from the client that's running the BEEFY protocol).
//! And in a runtime context, so that the BEEFY pallet can validate the
//! equivocation proofs in the extrinsic and report the offences.
//!
//! IMPORTANT:
//! When using this module for enabling equivocation reporting it is required
//! that the `ValidateUnsigned` for the BEEFY pallet is used in the runtime
//! definition.
use sp_std::prelude::*;
use beefy_primitives::{EquivocationProof, ValidatorSetId};
use codec::{self as codec, Decode, Encode};
use frame_support::{
log,
traits::{Get, KeyOwnerProofSystem},
};
use frame_system::pallet_prelude::BlockNumberFor;
use sp_runtime::{
transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
TransactionValidityError, ValidTransaction,
},
DispatchResult, Perbill, RuntimeAppPublic,
};
use sp_staking::{
offence::{Kind, Offence, OffenceError, ReportOffence},
SessionIndex,
};
use super::{Call, Config, Pallet, LOG_TARGET};
/// A trait with utility methods for handling equivocation reports in BEEFY.
/// 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: Config> {
/// The offence type used for reporting offences on valid equivocation reports.
type Offence: BeefyOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>;
/// The longevity, in blocks, that the equivocation report is valid for. When using the staking
/// pallet this should be equal to the bonding duration (in blocks, not eras).
type ReportLongevity: Get<u64>;
/// Report an offence proved by the given reporters.
fn report_offence(
reporters: Vec<T::AccountId>,
offence: Self::Offence,
) -> Result<(), OffenceError>;
/// Returns true if all of the offenders at the given time slot have already been reported.
fn is_known_offence(
offenders: &[T::KeyOwnerIdentification],
time_slot: &<Self::Offence as Offence<T::KeyOwnerIdentification>>::TimeSlot,
) -> bool;
/// Create and dispatch an equivocation report extrinsic.
fn submit_unsigned_equivocation_report(
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult;
/// Fetch the current block author id, if defined.
fn block_author() -> Option<T::AccountId>;
}
impl<T: Config> HandleEquivocation<T> for () {
type Offence = BeefyEquivocationOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>;
type ReportLongevity = ();
fn report_offence(
_reporters: Vec<T::AccountId>,
_offence: BeefyEquivocationOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>,
) -> Result<(), OffenceError> {
Ok(())
}
fn is_known_offence(
_offenders: &[T::KeyOwnerIdentification],
_time_slot: &BeefyTimeSlot<BlockNumberFor<T>>,
) -> bool {
true
}
fn submit_unsigned_equivocation_report(
_equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
_key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult {
Ok(())
}
fn block_author() -> Option<T::AccountId> {
None
}
}
/// 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<N, I, R, L, O = BeefyEquivocationOffence<N, I>> {
_phantom: sp_std::marker::PhantomData<(N, I, R, L, O)>,
}
impl<N, I, R, L, O> Default for EquivocationHandler<N, I, R, L, O> {
fn default() -> Self {
Self { _phantom: Default::default() }
}
}
impl<T, R, L, O> HandleEquivocation<T>
for EquivocationHandler<BlockNumberFor<T>, T::KeyOwnerIdentification, R, L, O>
where
// We use the authorship pallet to fetch the current block author and use
// `offchain::SendTransactionTypes` for unsigned extrinsic creation and
// submission.
T: Config + pallet_authorship::Config + frame_system::offchain::SendTransactionTypes<Call<T>>,
// A system for reporting offences after valid equivocation reports are
// processed.
R: ReportOffence<T::AccountId, T::KeyOwnerIdentification, O>,
// The longevity (in blocks) that the equivocation report is valid for. When using the staking
// pallet this should be the bonding duration.
L: Get<u64>,
// The offence type that should be used when reporting.
O: BeefyOffence<BlockNumberFor<T>, T::KeyOwnerIdentification>,
{
type Offence = O;
type ReportLongevity = L;
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
R::report_offence(reporters, offence)
}
fn is_known_offence(offenders: &[T::KeyOwnerIdentification], time_slot: &O::TimeSlot) -> bool {
R::is_known_offence(offenders, time_slot)
}
fn submit_unsigned_equivocation_report(
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResult {
use frame_system::offchain::SubmitTransaction;
let call = Call::report_equivocation_unsigned {
equivocation_proof: Box::new(equivocation_proof),
key_owner_proof,
};
match SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) {
Ok(()) => log::info!(target: LOG_TARGET, "Submitted BEEFY equivocation report.",),
Err(e) =>
log::error!(target: LOG_TARGET, "Error submitting equivocation report: {:?}", e,),
}
Ok(())
}
fn block_author() -> Option<T::AccountId> {
<pallet_authorship::Pallet<T>>::author()
}
}
/// 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 BeefyTimeSlot<N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode> {
// The order of these matters for `derive(Ord)`.
/// BEEFY Set ID.
pub set_id: ValidatorSetId,
/// Round number.
pub round: N,
}
/// Methods for the `ValidateUnsigned` implementation:
/// It restricts calls to `report_equivocation_unsigned` to local calls (i.e. extrinsics generated
/// on this node) or that already in a block. This guarantees that only block authors can include
/// unsigned equivocation reports.
impl<T: Config> Pallet<T> {
pub fn validate_unsigned(source: TransactionSource, call: &Call<T>) -> TransactionValidity {
if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call {
// discard equivocation report not coming from the local node
match source {
TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ },
_ => {
log::warn!(
target: LOG_TARGET,
"rejecting unsigned report equivocation transaction because it is not local/in-block."
);
return InvalidTransaction::Call.into()
},
}
// check report staleness
is_known_offence::<T>(equivocation_proof, key_owner_proof)?;
let longevity =
<T::HandleEquivocation as HandleEquivocation<T>>::ReportLongevity::get();
ValidTransaction::with_tag_prefix("BeefyEquivocation")
// We assign the maximum priority for any equivocation report.
.priority(TransactionPriority::MAX)
// Only one equivocation report for the same offender at the same slot.
.and_provides((
equivocation_proof.offender_id().clone(),
equivocation_proof.set_id(),
*equivocation_proof.round_number(),
))
.longevity(longevity)
// We don't propagate this. This can never be included on a remote node.
.propagate(false)
.build()
} else {
InvalidTransaction::Call.into()
}
}
pub fn pre_dispatch(call: &Call<T>) -> Result<(), TransactionValidityError> {
if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call {
is_known_offence::<T>(equivocation_proof, key_owner_proof)
} else {
Err(InvalidTransaction::Call.into())
}
}
}
fn is_known_offence<T: Config>(
equivocation_proof: &EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: &T::KeyOwnerProof,
) -> Result<(), TransactionValidityError> {
// check the membership proof to extract the offender's id,
// equivocation validity will be fully checked during the call.
let key = (beefy_primitives::KEY_TYPE, equivocation_proof.offender_id().clone());
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof.clone())
.ok_or(InvalidTransaction::BadProof)?;
// check if the offence has already been reported,
// and if so then we can discard the report.
let time_slot = <T::HandleEquivocation as HandleEquivocation<T>>::Offence::new_time_slot(
equivocation_proof.set_id(),
*equivocation_proof.round_number(),
);
let is_known_offence = T::HandleEquivocation::is_known_offence(&[offender], &time_slot);
if is_known_offence {
Err(InvalidTransaction::Stale.into())
} else {
Ok(())
}
}
/// A BEEFY equivocation offence report.
pub struct BeefyEquivocationOffence<N, FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
/// Time slot at which this incident happened.
pub time_slot: BeefyTimeSlot<N>,
/// 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 BEEFY offences and must also
/// implement the `Offence` trait. This trait provides a constructor that is
/// provided all available data during processing of BEEFY equivocations.
pub trait BeefyOffence<N, FullIdentification>: Offence<FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
/// Create a new BEEFY offence using the given equivocation details.
fn new(
session_index: SessionIndex,
validator_set_count: u32,
offender: FullIdentification,
set_id: ValidatorSetId,
round: N,
) -> Self;
/// Create a new BEEFY offence time slot.
fn new_time_slot(set_id: ValidatorSetId, round: N) -> Self::TimeSlot;
}
impl<N, FullIdentification: Clone> BeefyOffence<N, FullIdentification>
for BeefyEquivocationOffence<N, FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
fn new(
session_index: SessionIndex,
validator_set_count: u32,
offender: FullIdentification,
set_id: ValidatorSetId,
round: N,
) -> Self {
BeefyEquivocationOffence {
session_index,
validator_set_count,
offender,
time_slot: BeefyTimeSlot { set_id, round },
}
}
fn new_time_slot(set_id: ValidatorSetId, round: N) -> Self::TimeSlot {
BeefyTimeSlot { set_id, round }
}
}
impl<N, FullIdentification: Clone> Offence<FullIdentification>
for BeefyEquivocationOffence<N, FullIdentification>
where
N: Copy + Clone + PartialOrd + Ord + Eq + PartialEq + Encode + Decode,
{
const ID: Kind = *b"beefy:equivocati";
type TimeSlot = BeefyTimeSlot<N>;
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(&self, offenders_count: u32) -> Perbill {
// the formula is min((3k / n)^2, 1)
let x = Perbill::from_rational(3 * offenders_count, self.validator_set_count);
// _ ^ 2
x.square()
}
}
+236 -16
View File
@@ -20,35 +20,48 @@
use codec::{Encode, MaxEncodedLen};
use frame_support::{
dispatch::{DispatchResultWithPostInfo, Pays},
log,
traits::{Get, OneSessionHandler},
pallet_prelude::*,
traits::{Get, KeyOwnerProofSystem, OneSessionHandler},
weights::Weight,
BoundedSlice, BoundedVec, Parameter,
};
use frame_system::{
ensure_none, ensure_signed,
pallet_prelude::{BlockNumberFor, OriginFor},
};
use sp_runtime::{
generic::DigestItem,
traits::{IsMember, Member},
RuntimeAppPublic,
KeyTypeId, RuntimeAppPublic,
};
use sp_session::{GetSessionNumber, GetValidatorCount};
use sp_staking::SessionIndex;
use sp_std::prelude::*;
use beefy_primitives::{
AuthorityIndex, ConsensusLog, OnNewValidatorSet, ValidatorSet, BEEFY_ENGINE_ID,
GENESIS_AUTHORITY_SET_ID,
AuthorityIndex, BeefyAuthorityId, ConsensusLog, EquivocationProof, OnNewValidatorSet,
ValidatorSet, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID,
};
mod default_weights;
mod equivocation;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub use crate::equivocation::{
BeefyEquivocationOffence, BeefyOffence, BeefyTimeSlot, EquivocationHandler, HandleEquivocation,
};
pub use pallet::*;
const LOG_TARGET: &str = "runtime::beefy";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::BlockNumberFor;
#[pallet::config]
@@ -56,10 +69,35 @@ pub mod pallet {
/// Authority identifier type
type BeefyId: Member
+ Parameter
+ RuntimeAppPublic
// todo: use custom signature hashing type instead of hardcoded `Keccak256`
+ BeefyAuthorityId<sp_runtime::traits::Keccak256>
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
/// 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, Self::BeefyId),
Proof = Self::KeyOwnerProof,
IdentificationTuple = Self::KeyOwnerIdentification,
>;
/// 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;
/// 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 use this pallet's `ValidateUnsigned` in the runtime
/// definition.
type HandleEquivocation: HandleEquivocation<Self>;
/// The maximum number of authorities that can be added.
type MaxAuthorities: Get<u32>;
@@ -69,6 +107,9 @@ pub mod pallet {
/// externally apart from having it in the storage. For instance you might cache a light
/// weight MMR root over validators and make it available for Light Clients.
type OnNewValidatorSet: OnNewValidatorSet<<Self as Config>::BeefyId>;
/// Weights for this pallet.
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
@@ -92,6 +133,15 @@ pub mod pallet {
pub(super) type NextAuthorities<T: Config> =
StorageValue<_, BoundedVec<T::BeefyId, T::MaxAuthorities>, ValueQuery>;
/// A mapping from BEEFY set ID to the index of the *most recent* session for which its
/// members were responsible.
///
/// TWOX-NOTE: `ValidatorSetId` is not under user control.
#[pallet::storage]
#[pallet::getter(fn session_for_set)]
pub(super) type SetIdSession<T: Config> =
StorageMap<_, Twox64Concat, beefy_primitives::ValidatorSetId, SessionIndex>;
/// Block number where BEEFY consensus is enabled/started.
/// If changing this, make sure `Self::ValidatorSetId` is also reset to
/// `GENESIS_AUTHORITY_SET_ID` in the state of the new block number configured here.
@@ -124,13 +174,90 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
Pallet::<T>::initialize_authorities(&self.authorities)
Pallet::<T>::initialize(&self.authorities)
// we panic here as runtime maintainers can simply reconfigure genesis and restart
// the chain easily
.expect("Authorities vec too big");
<GenesisBlock<T>>::put(&self.genesis_block);
}
}
#[pallet::error]
pub enum Error<T> {
/// A key ownership proof provided as part of an equivocation report is invalid.
InvalidKeyOwnershipProof,
/// An equivocation proof provided as part of an equivocation report is invalid.
InvalidEquivocationProof,
/// A given equivocation report is valid but already previously reported.
DuplicateOffenceReport,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// 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.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::report_equivocation(key_owner_proof.validator_count()))]
pub fn report_equivocation(
origin: OriginFor<T>,
equivocation_proof: Box<
EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
let reporter = ensure_signed(origin)?;
Self::do_report_equivocation(Some(reporter), *equivocation_proof, key_owner_proof)
}
/// 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.
///
/// This extrinsic must be called unsigned and it is expected that only
/// block authors will call it (validated in `ValidateUnsigned`), as such
/// if the block author is defined it will be defined as the equivocation
/// reporter.
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::report_equivocation(key_owner_proof.validator_count()))]
pub fn report_equivocation_unsigned(
origin: OriginFor<T>,
equivocation_proof: Box<
EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
ensure_none(origin)?;
Self::do_report_equivocation(
T::HandleEquivocation::block_author(),
*equivocation_proof,
key_owner_proof,
)
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::pre_dispatch(call)
}
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
Self::validate_unsigned(source, call)
}
}
}
impl<T: Config> Pallet<T> {
@@ -141,6 +268,24 @@ impl<T: Config> Pallet<T> {
ValidatorSet::<T::BeefyId>::new(validators, id)
}
/// Submits an extrinsic to report an equivocation. This method will create
/// an unsigned extrinsic with a call to `report_equivocation_unsigned` and
/// will push the transaction to the pool. Only useful in an offchain context.
pub fn submit_unsigned_equivocation_report(
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> Option<()> {
T::HandleEquivocation::submit_unsigned_equivocation_report(
equivocation_proof,
key_owner_proof,
)
.ok()
}
fn change_authorities(
new: BoundedVec<T::BeefyId, T::MaxAuthorities>,
queued: BoundedVec<T::BeefyId, T::MaxAuthorities>,
@@ -169,7 +314,7 @@ impl<T: Config> Pallet<T> {
}
}
fn initialize_authorities(authorities: &Vec<T::BeefyId>) -> Result<(), ()> {
fn initialize(authorities: &Vec<T::BeefyId>) -> Result<(), ()> {
if authorities.is_empty() {
return Ok(())
}
@@ -199,15 +344,80 @@ impl<T: Config> Pallet<T> {
);
}
}
// NOTE: initialize first session of first set. this is necessary for
// the genesis set and session since we only update the set -> session
// mapping whenever a new session starts, i.e. through `on_new_session`.
SetIdSession::<T>::insert(0, 0);
Ok(())
}
fn do_report_equivocation(
reporter: Option<T::AccountId>,
equivocation_proof: EquivocationProof<
BlockNumberFor<T>,
T::BeefyId,
<T::BeefyId as RuntimeAppPublic>::Signature,
>,
key_owner_proof: T::KeyOwnerProof,
) -> DispatchResultWithPostInfo {
// We check the equivocation within the context of its set id (and
// associated session) and round. We also need to know the validator
// set count at the time of the offence since it is required to calculate
// the slash amount.
let set_id = equivocation_proof.set_id();
let round = *equivocation_proof.round_number();
let offender_id = equivocation_proof.offender_id().clone();
let session_index = key_owner_proof.session();
let validator_count = key_owner_proof.validator_count();
// validate the key ownership proof extracting the id of the offender.
let offender = T::KeyOwnerProofSystem::check_proof(
(beefy_primitives::KEY_TYPE, offender_id),
key_owner_proof,
)
.ok_or(Error::<T>::InvalidKeyOwnershipProof)?;
// validate equivocation proof (check votes are different and signatures are valid).
if !beefy_primitives::check_equivocation_proof(&equivocation_proof) {
return Err(Error::<T>::InvalidEquivocationProof.into())
}
// check that the session id for the membership proof is within the
// bounds of the set id reported in the equivocation.
let set_id_session_index =
Self::session_for_set(set_id).ok_or(Error::<T>::InvalidEquivocationProof)?;
if session_index != set_id_session_index {
return Err(Error::<T>::InvalidEquivocationProof.into())
}
// report to the offences module rewarding the sender.
T::HandleEquivocation::report_offence(
reporter.into_iter().collect(),
<T::HandleEquivocation as HandleEquivocation<T>>::Offence::new(
session_index,
validator_count,
offender,
set_id,
round,
),
)
.map_err(|_| Error::<T>::DuplicateOffenceReport)?;
// waive the fee since the report is valid and beneficial
Ok(Pays::No.into())
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
type Public = T::BeefyId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T>
where
T: pallet_session::Config,
{
type Key = T::BeefyId;
fn on_genesis_session<'a, I: 'a>(validators: I)
@@ -217,7 +427,7 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
let authorities = validators.map(|(_, k)| k).collect::<Vec<_>>();
// we panic here as runtime maintainers can simply reconfigure genesis and restart the
// chain easily
Self::initialize_authorities(&authorities).expect("Authorities vec too big");
Self::initialize(&authorities).expect("Authorities vec too big");
}
fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, queued_validators: I)
@@ -227,9 +437,10 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
let next_authorities = validators.map(|(_, k)| k).collect::<Vec<_>>();
if next_authorities.len() as u32 > T::MaxAuthorities::get() {
log::error!(
target: "runtime::beefy",
target: LOG_TARGET,
"authorities list {:?} truncated to length {}",
next_authorities, T::MaxAuthorities::get(),
next_authorities,
T::MaxAuthorities::get(),
);
}
let bounded_next_authorities =
@@ -238,9 +449,10 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
let next_queued_authorities = queued_validators.map(|(_, k)| k).collect::<Vec<_>>();
if next_queued_authorities.len() as u32 > T::MaxAuthorities::get() {
log::error!(
target: "runtime::beefy",
target: LOG_TARGET,
"queued authorities list {:?} truncated to length {}",
next_queued_authorities, T::MaxAuthorities::get(),
next_queued_authorities,
T::MaxAuthorities::get(),
);
}
let bounded_next_queued_authorities =
@@ -249,6 +461,10 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
// Always issue a change on each `session`, even if validator set hasn't changed.
// We want to have at least one BEEFY mandatory block per session.
Self::change_authorities(bounded_next_authorities, bounded_next_queued_authorities);
// Update the mapping for the new set id that corresponds to the latest session (i.e. now).
let session_index = <pallet_session::Pallet<T>>::current_index();
SetIdSession::<T>::insert(Self::validator_set_id(), &session_index);
}
fn on_disabled(i: u32) {
@@ -266,3 +482,7 @@ impl<T: Config> IsMember<T::BeefyId> for Pallet<T> {
Self::authorities().iter().any(|id| id == authority_id)
}
}
pub trait WeightInfo {
fn report_equivocation(validator_count: u32) -> Weight;
}
+203 -28
View File
@@ -17,24 +17,33 @@
use std::vec;
use frame_election_provider_support::{onchain, SequentialPhragmen};
use frame_support::{
construct_runtime, parameter_types,
sp_io::TestExternalities,
traits::{ConstU16, ConstU32, ConstU64, GenesisBuild},
traits::{
ConstU16, ConstU32, ConstU64, GenesisBuild, KeyOwnerProofSystem, OnFinalize, OnInitialize,
},
BasicExternalities,
};
use sp_core::H256;
use pallet_session::historical as pallet_session_historical;
use sp_core::{crypto::KeyTypeId, ConstU128, H256};
use sp_runtime::{
app_crypto::ecdsa::Public,
curve::PiecewiseLinear,
impl_opaque_keys,
testing::Header,
traits::{BlakeTwo256, ConvertInto, IdentityLookup, OpaqueKeys},
testing::{Header, TestXt},
traits::{BlakeTwo256, IdentityLookup, OpaqueKeys},
Perbill,
};
use sp_staking::{EraIndex, SessionIndex};
use crate as pallet_beefy;
pub use beefy_primitives::{crypto::AuthorityId as BeefyId, ConsensusLog, BEEFY_ENGINE_ID};
pub use beefy_primitives::{
crypto::{AuthorityId as BeefyId, AuthoritySignature as BeefySignature},
ConsensusLog, EquivocationProof, BEEFY_ENGINE_ID,
};
impl_opaque_keys! {
pub struct MockSessionKeys {
@@ -51,9 +60,15 @@ construct_runtime!(
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Beefy: pallet_beefy::{Pallet, Config<T>, Storage},
Session: pallet_session::{Pallet, Call, Storage, Event, Config<T>},
System: frame_system,
Authorship: pallet_authorship,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
Beefy: pallet_beefy,
Staking: pallet_staking,
Session: pallet_session,
Offences: pallet_offences,
Historical: pallet_session_historical,
}
);
@@ -75,7 +90,7 @@ impl frame_system::Config for Test {
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type AccountData = pallet_balances::AccountData<u128>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
@@ -84,10 +99,34 @@ impl frame_system::Config for Test {
type MaxConsumers = ConstU32<16>;
}
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
where
RuntimeCall: From<C>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = TestXt<RuntimeCall, ()>;
}
parameter_types! {
pub const Period: u64 = 1;
pub const ReportLongevity: u64 =
BondingDuration::get() as u64 * SessionsPerEra::get() as u64 * Period::get();
}
impl pallet_beefy::Config for Test {
type BeefyId = BeefyId;
type KeyOwnerProofSystem = Historical;
type KeyOwnerProof =
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, BeefyId)>>::Proof;
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
KeyTypeId,
BeefyId,
)>>::IdentificationTuple;
type HandleEquivocation =
super::EquivocationHandler<u64, Self::KeyOwnerIdentification, Offences, ReportLongevity>;
type MaxAuthorities = ConstU32<100>;
type OnNewValidatorSet = ();
type WeightInfo = ();
}
parameter_types! {
@@ -97,29 +136,107 @@ parameter_types! {
impl pallet_session::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = u64;
type ValidatorIdOf = ConvertInto;
type ValidatorIdOf = pallet_staking::StashOf<Self>;
type ShouldEndSession = pallet_session::PeriodicSessions<ConstU64<1>, ConstU64<0>>;
type NextSessionRotation = pallet_session::PeriodicSessions<ConstU64<1>, ConstU64<0>>;
type SessionManager = MockSessionManager;
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Self, Staking>;
type SessionHandler = <MockSessionKeys as OpaqueKeys>::KeyTypeIdProviders;
type Keys = MockSessionKeys;
type WeightInfo = ();
}
pub struct MockSessionManager;
impl pallet_session::historical::Config for Test {
type FullIdentification = pallet_staking::Exposure<u64, u128>;
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
}
impl pallet_session::SessionManager<u64> for MockSessionManager {
fn end_session(_: sp_staking::SessionIndex) {}
fn start_session(_: sp_staking::SessionIndex) {}
fn new_session(idx: sp_staking::SessionIndex) -> Option<Vec<u64>> {
if idx == 0 || idx == 1 {
Some(vec![1, 2])
} else if idx == 2 {
Some(vec![3, 4])
} else {
None
}
}
impl pallet_authorship::Config for Test {
type FindAuthor = ();
type EventHandler = ();
}
impl pallet_balances::Config for Test {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = u128;
type DustRemoval = ();
type RuntimeEvent = RuntimeEvent;
type ExistentialDeposit = ConstU128<1>;
type AccountStore = System;
type WeightInfo = ();
}
impl pallet_timestamp::Config for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ConstU64<3>;
type WeightInfo = ();
}
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 RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
}
pub struct OnChainSeqPhragmen;
impl onchain::Config for OnChainSeqPhragmen {
type System = Test;
type Solver = SequentialPhragmen<u64, Perbill>;
type DataProvider = Staking;
type WeightInfo = ();
type MaxWinners = ConstU32<100>;
type VotersBound = ConstU32<{ u32::MAX }>;
type TargetsBound = ConstU32<{ u32::MAX }>;
}
impl pallet_staking::Config for Test {
type MaxNominations = ConstU32<16>;
type RewardRemainder = ();
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
type Slash = ();
type Reward = ();
type SessionsPerEra = SessionsPerEra;
type BondingDuration = BondingDuration;
type SlashDeferDuration = ();
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type SessionInterface = Self;
type UnixTime = pallet_timestamp::Pallet<Test>;
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type MaxNominatorRewardedPerValidator = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type NextNewSession = Session;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
type TargetList = pallet_staking::UseValidatorsMap<Self>;
type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>;
type OnStakerSlash = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
}
impl pallet_offences::Config for Test {
type RuntimeEvent = RuntimeEvent;
type IdentificationTuple = pallet_session::historical::IdentificationTuple<Self>;
type OnOffenceHandler = Staking;
}
// Note, that we can't use `UintAuthorityId` here. Reason is that the implementation
@@ -134,20 +251,27 @@ pub fn mock_beefy_id(id: u8) -> BeefyId {
BeefyId::from(pk)
}
pub fn mock_authorities(vec: Vec<u8>) -> Vec<(u64, BeefyId)> {
vec.into_iter().map(|id| ((id as u64), mock_beefy_id(id))).collect()
pub fn mock_authorities(vec: Vec<u8>) -> Vec<BeefyId> {
vec.into_iter().map(|id| mock_beefy_id(id)).collect()
}
pub fn new_test_ext(ids: Vec<u8>) -> TestExternalities {
new_test_ext_raw_authorities(mock_authorities(ids))
}
pub fn new_test_ext_raw_authorities(authorities: Vec<(u64, BeefyId)>) -> TestExternalities {
pub fn new_test_ext_raw_authorities(authorities: Vec<BeefyId>) -> TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
let balances: Vec<_> = (0..authorities.len()).map(|i| (i as u64, 10_000_000)).collect();
pallet_balances::GenesisConfig::<Test> { balances }
.assimilate_storage(&mut t)
.unwrap();
let session_keys: Vec<_> = authorities
.iter()
.map(|id| (id.0 as u64, id.0 as u64, MockSessionKeys { dummy: id.1.clone() }))
.enumerate()
.map(|(i, k)| (i as u64, i as u64, MockSessionKeys { dummy: k.clone() }))
.collect();
BasicExternalities::execute_with_storage(&mut t, || {
@@ -160,5 +284,56 @@ pub fn new_test_ext_raw_authorities(authorities: Vec<(u64, BeefyId)>) -> TestExt
.assimilate_storage(&mut t)
.unwrap();
// controllers are the index + 1000
let stakers: Vec<_> = (0..authorities.len())
.map(|i| {
(i as u64, i as u64 + 1000, 10_000, pallet_staking::StakerStatus::<u64>::Validator)
})
.collect();
let staking_config = pallet_staking::GenesisConfig::<Test> {
stakers,
validator_count: 2,
force_era: pallet_staking::Forcing::ForceNew,
minimum_validator_count: 0,
invulnerables: vec![],
..Default::default()
};
staking_config.assimilate_storage(&mut t).unwrap();
t.into()
}
pub fn start_session(session_index: SessionIndex) {
for i in Session::current_index()..session_index {
System::on_finalize(System::block_number());
Session::on_finalize(System::block_number());
Staking::on_finalize(System::block_number());
Beefy::on_finalize(System::block_number());
let parent_hash = if System::block_number() > 1 {
let hdr = System::finalize();
hdr.hash()
} else {
System::parent_hash()
};
System::reset_events();
System::initialize(&(i as u64 + 1), &parent_hash, &Default::default());
System::set_block_number((i + 1).into());
Timestamp::set_timestamp(System::block_number() * 6000);
System::on_initialize(System::block_number());
Session::on_initialize(System::block_number());
Staking::on_initialize(System::block_number());
Beefy::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));
}
+634 -13
View File
@@ -17,14 +17,21 @@
use std::vec;
use beefy_primitives::ValidatorSet;
use beefy_primitives::{
check_equivocation_proof, generate_equivocation_proof, known_payloads::MMR_ROOT_ID,
Keyring as BeefyKeyring, Payload, ValidatorSet,
};
use codec::Encode;
use sp_runtime::DigestItem;
use frame_support::traits::OnInitialize;
use frame_support::{
assert_err, assert_ok,
dispatch::{GetDispatchInfo, Pays},
traits::{Currency, KeyOwnerProofSystem, OnInitialize},
};
use crate::mock::*;
use crate::{mock::*, Call, Config, Error, Weight, WeightInfo};
fn init_block(block: u64) {
System::set_block_number(block);
@@ -37,12 +44,13 @@ pub fn beefy_log(log: ConsensusLog<BeefyId>) -> DigestItem {
#[test]
fn genesis_session_initializes_authorities() {
let want = vec![mock_beefy_id(1), mock_beefy_id(2), mock_beefy_id(3), mock_beefy_id(4)];
let authorities = mock_authorities(vec![1, 2, 3, 4]);
let want = authorities.clone();
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
new_test_ext_raw_authorities(authorities).execute_with(|| {
let authorities = Beefy::authorities();
assert!(authorities.len() == 2);
assert_eq!(authorities.len(), 4);
assert_eq!(want[0], authorities[0]);
assert_eq!(want[1], authorities[1]);
@@ -50,7 +58,7 @@ fn genesis_session_initializes_authorities() {
let next_authorities = Beefy::next_authorities();
assert!(next_authorities.len() == 2);
assert_eq!(next_authorities.len(), 4);
assert_eq!(want[0], next_authorities[0]);
assert_eq!(want[1], next_authorities[1]);
});
@@ -58,6 +66,9 @@ fn genesis_session_initializes_authorities() {
#[test]
fn session_change_updates_authorities() {
let authorities = mock_authorities(vec![1, 2, 3, 4]);
let want_validators = authorities.clone();
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
assert!(0 == Beefy::validator_set_id());
@@ -66,7 +77,7 @@ fn session_change_updates_authorities() {
assert!(1 == Beefy::validator_set_id());
let want = beefy_log(ConsensusLog::AuthoritiesChange(
ValidatorSet::new(vec![mock_beefy_id(1), mock_beefy_id(2)], 1).unwrap(),
ValidatorSet::new(want_validators, 1).unwrap(),
));
let log = System::digest().logs[0].clone();
@@ -77,7 +88,7 @@ fn session_change_updates_authorities() {
assert!(2 == Beefy::validator_set_id());
let want = beefy_log(ConsensusLog::AuthoritiesChange(
ValidatorSet::new(vec![mock_beefy_id(3), mock_beefy_id(4)], 2).unwrap(),
ValidatorSet::new(vec![mock_beefy_id(2), mock_beefy_id(4)], 2).unwrap(),
));
let log = System::digest().logs[1].clone();
@@ -92,16 +103,18 @@ fn session_change_updates_next_authorities() {
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
let next_authorities = Beefy::next_authorities();
assert!(next_authorities.len() == 2);
assert_eq!(next_authorities.len(), 4);
assert_eq!(want[0], next_authorities[0]);
assert_eq!(want[1], next_authorities[1]);
assert_eq!(want[2], next_authorities[2]);
assert_eq!(want[3], next_authorities[3]);
init_block(1);
let next_authorities = Beefy::next_authorities();
assert!(next_authorities.len() == 2);
assert_eq!(want[2], next_authorities[0]);
assert_eq!(next_authorities.len(), 2);
assert_eq!(want[1], next_authorities[0]);
assert_eq!(want[3], next_authorities[1]);
});
}
@@ -126,6 +139,10 @@ fn validator_set_updates_work() {
new_test_ext(vec![1, 2, 3, 4]).execute_with(|| {
let vs = Beefy::validator_set().unwrap();
assert_eq!(vs.id(), 0u64);
assert_eq!(want[0], vs.validators()[0]);
assert_eq!(want[1], vs.validators()[1]);
assert_eq!(want[2], vs.validators()[2]);
assert_eq!(want[3], vs.validators()[3]);
init_block(1);
@@ -140,7 +157,611 @@ fn validator_set_updates_work() {
let vs = Beefy::validator_set().unwrap();
assert_eq!(vs.id(), 2u64);
assert_eq!(want[2], vs.validators()[0]);
assert_eq!(want[1], vs.validators()[0]);
assert_eq!(want[3], vs.validators()[1]);
});
}
/// Returns a list with 3 authorities with known keys:
/// Alice, Bob and Charlie.
pub fn test_authorities() -> Vec<BeefyId> {
let authorities = vec![BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie];
authorities.into_iter().map(|id| id.public()).collect()
}
#[test]
fn should_sign_and_verify() {
use sp_runtime::traits::Keccak256;
let set_id = 3;
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// generate an equivocation proof, with two votes in the same round for
// same payload signed by the same key
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes in different rounds for
// different payloads signed by the same key
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
(2, payload2.clone(), set_id, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes by different authorities
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Alice),
(1, payload2.clone(), set_id, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes in different set ids
let equivocation_proof = generate_equivocation_proof(
(1, payload1.clone(), set_id, &BeefyKeyring::Bob),
(1, payload2.clone(), set_id + 1, &BeefyKeyring::Bob),
);
// expect invalid equivocation proof
assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
// generate an equivocation proof, with two votes in the same round for
// different payloads signed by the same key
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
let equivocation_proof = generate_equivocation_proof(
(1, payload1, set_id, &BeefyKeyring::Bob),
(1, payload2, set_id, &BeefyKeyring::Bob),
);
// expect valid equivocation proof
assert!(check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof));
}
#[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 block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let validators = Session::validators();
// make sure that all validators have the same balance
for validator in &validators {
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(1, validator),
pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
);
}
assert_eq!(authorities.len(), 2);
let equivocation_authority_index = 1;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// generate an equivocation proof, with two votes in the same round for
// different payloads signed by the same key
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
// create the key ownership proof
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
// report the equivocation and the tx should be dispatched successfully
assert_ok!(Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),);
start_era(2);
// check that the balance of 0-th validator is slashed 100%.
let equivocation_validator_id = validators[equivocation_authority_index];
assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
assert_eq!(
Staking::eras_stakers(2, equivocation_validator_id),
pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
);
// check that the balances of all other validators are left intact.
for validator in &validators {
if *validator == equivocation_validator_id {
continue
}
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(2, validator),
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 block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let validators = Session::validators();
let old_set_id = validator_set.id();
assert_eq!(authorities.len(), 2);
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
// create the key ownership proof in the "old" set
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
start_era(2);
// make sure that all authorities have the same balance
for validator in &validators {
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(2, validator),
pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
);
}
let validator_set = Beefy::validator_set().unwrap();
let new_set_id = validator_set.id();
assert_eq!(old_set_id + 3, new_set_id);
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// generate an equivocation proof for the old set,
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, old_set_id, &equivocation_keyring),
(block_num, payload2, old_set_id, &equivocation_keyring),
);
// report the equivocation and the tx should be dispatched successfully
assert_ok!(Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),);
start_era(3);
// check that the balance of 0-th validator is slashed 100%.
let equivocation_validator_id = validators[equivocation_authority_index];
assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
assert_eq!(
Staking::eras_stakers(3, equivocation_validator_id),
pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
);
// check that the balances of all other validators are left intact.
for validator in &validators {
if *validator == equivocation_validator_id {
continue
}
assert_eq!(Balances::total_balance(validator), 10_000_000);
assert_eq!(Staking::slashable_balance_of(validator), 10_000);
assert_eq!(
Staking::eras_stakers(3, validator),
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 block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// generate an equivocation for a future set
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id + 1, &equivocation_keyring),
(block_num, payload2, set_id + 1, &equivocation_keyring),
);
// the call for reporting the equivocation should error
assert_err!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),
Error::<Test>::InvalidEquivocationProof,
);
});
}
#[test]
fn report_equivocation_invalid_session() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
// generate a key ownership proof at current era set id
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
start_era(2);
let set_id = Beefy::validator_set().unwrap().id();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// generate an equivocation proof at following era set id = 2
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &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!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
),
Error::<Test>::InvalidEquivocationProof,
);
});
}
#[test]
fn report_equivocation_invalid_key_owner_proof() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let invalid_owner_authority_index = 1;
let invalid_owner_key = &authorities[invalid_owner_authority_index];
// generate a key ownership proof for the authority at index 1
let invalid_key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &invalid_owner_key)).unwrap();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// generate an equivocation proof for the authority at index 0
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id + 1, &equivocation_keyring),
(block_num, payload2, set_id + 1, &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!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
invalid_key_owner_proof,
),
Error::<Test>::InvalidKeyOwnershipProof,
);
});
}
#[test]
fn report_equivocation_invalid_equivocation_proof() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
// generate a key ownership proof at set id in era 1
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
let assert_invalid_equivocation_proof = |equivocation_proof| {
assert_err!(
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof.clone(),
),
Error::<Test>::InvalidEquivocationProof,
);
};
start_era(2);
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
// both votes target the same block number and payload,
// there is no equivocation.
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num, payload1.clone(), set_id, &equivocation_keyring),
));
// votes targeting different rounds, there is no equivocation.
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num + 1, payload2.clone(), set_id, &equivocation_keyring),
));
// votes signed with different authority keys
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num, payload1.clone(), set_id, &BeefyKeyring::Charlie),
));
// votes signed with a key that isn't part of the authority set
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1.clone(), set_id, &equivocation_keyring),
(block_num, payload1.clone(), set_id, &BeefyKeyring::Dave),
));
// votes targeting different set ids
assert_invalid_equivocation_proof(generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id + 1, &equivocation_keyring),
));
});
}
#[test]
fn report_equivocation_validate_unsigned_prevents_duplicates() {
use sp_runtime::transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
ValidTransaction,
};
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
// generate and report an equivocation for the validator at index 0
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
let call = Call::report_equivocation_unsigned {
equivocation_proof: Box::new(equivocation_proof.clone()),
key_owner_proof: key_owner_proof.clone(),
};
// only local/inblock reports are allowed
assert_eq!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
TransactionSource::External,
&call,
),
InvalidTransaction::Call.into(),
);
// the transaction is valid when passed as local
let tx_tag = (equivocation_key, set_id, 3u64);
assert_eq!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call,
),
TransactionValidity::Ok(ValidTransaction {
priority: TransactionPriority::max_value(),
requires: vec![],
provides: vec![("BeefyEquivocation", tx_tag).encode()],
longevity: ReportLongevity::get(),
propagate: false,
})
);
// the pre dispatch checks should also pass
assert_ok!(<Beefy as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&call));
// we submit the report
Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
)
.unwrap();
// the report should now be considered stale and the transaction is invalid
// the check for staleness should be done on both `validate_unsigned` and on `pre_dispatch`
assert_err!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call,
),
InvalidTransaction::Stale,
);
assert_err!(
<Beefy as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&call),
InvalidTransaction::Stale,
);
});
}
#[test]
fn report_equivocation_has_valid_weight() {
// the weight depends on the size of the validator set,
// but there's a lower bound of 100 validators.
assert!((1..=100)
.map(<Test as Config>::WeightInfo::report_equivocation)
.collect::<Vec<_>>()
.windows(2)
.all(|w| w[0] == w[1]));
// after 100 validators the weight should keep increasing
// with every extra validator.
assert!((100..=1000)
.map(<Test as Config>::WeightInfo::report_equivocation)
.collect::<Vec<_>>()
.windows(2)
.all(|w| w[0].ref_time() < w[1].ref_time()));
}
#[test]
fn valid_equivocation_reports_dont_pay_fees() {
let authorities = test_authorities();
new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);
let block_num = System::block_number();
let validator_set = Beefy::validator_set().unwrap();
let authorities = validator_set.validators();
let set_id = validator_set.id();
let equivocation_authority_index = 0;
let equivocation_key = &authorities[equivocation_authority_index];
let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap();
// generate equivocation proof
let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]);
let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]);
let equivocation_proof = generate_equivocation_proof(
(block_num, payload1, set_id, &equivocation_keyring),
(block_num, payload2, set_id, &equivocation_keyring),
);
// create the key ownership proof.
let key_owner_proof =
Historical::prove((beefy_primitives::KEY_TYPE, &equivocation_key)).unwrap();
// check the dispatch info for the call.
let info = Call::<Test>::report_equivocation_unsigned {
equivocation_proof: Box::new(equivocation_proof.clone()),
key_owner_proof: key_owner_proof.clone(),
}
.get_dispatch_info();
// it should have non-zero weight and the fee has to be paid.
assert!(info.weight.any_gt(Weight::zero()));
assert_eq!(info.pays_fee, Pays::Yes);
// report the equivocation.
let post_info = Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof.clone()),
key_owner_proof.clone(),
)
.unwrap();
// the original weight should be kept, but given that the report
// is valid the fee is waived.
assert!(post_info.actual_weight.is_none());
assert_eq!(post_info.pays_fee, Pays::No);
// report the equivocation again which is invalid now since it is
// duplicate.
let post_info = Beefy::report_equivocation_unsigned(
RuntimeOrigin::none(),
Box::new(equivocation_proof),
key_owner_proof,
)
.err()
.unwrap()
.post_info;
// the fee is not waived and the original weight is kept.
assert!(post_info.actual_weight.is_none());
assert_eq!(post_info.pays_fee, Pays::Yes);
})
}