mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-14 14:31:13 +00:00
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:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user