mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-20 18:35:41 +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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user