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

* client/beefy: simplify self_vote logic

* client/beefy: migrate to new state version

* client/beefy: detect equivocated votes

* fix typos

* sp-beefy: add equivocation primitives

* client/beefy: refactor vote processing

* fix version migration for new rounds struct

* client/beefy: track equivocations and create proofs

* client/beefy: adjust tests for new voting logic

* sp-beefy: fix commitment ordering and equality

* client/beefy: simplify handle_vote() a bit

* client/beefy: add simple equivocation test

* client/beefy: submit equivocation proof - WIP

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

* frame/beefy: report equivocation logic - part 2

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

* frame/beefy: impl ValidateUnsigned for equivocations reporting

* client/beefy: submit report equivocation unsigned extrinsic

* primitives/beefy: fix tests

* frame/beefy: add default weights

* frame/beefy: fix tests

* client/beefy: fix tests

* frame/beefy-mmr: fix tests

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

* sp-beefy: make test Keyring useable in pallet

* frame/beefy: add basic equivocation test

* frame/beefy: test verify equivocation results in slashing

* frame/beefy: test report_equivocation_old_set

* frame/beefy: add more equivocation tests

* sp-beefy: fix docs

* beefy: simplify equivocations and fix tests

* client/beefy: address review comments

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

* client/beefy: fixes after merge master

* fix missed merge damage

* client/beefy: add test for reporting equivocations

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

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

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

* client/beefy: add negative test for equivocation reports

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

* impl review suggestions

* client/beefy: add equivocation metrics

---------

Signed-off-by: acatangiu <adrian@parity.io>
Co-authored-by: parity-processbot <>
This commit is contained in:
Adrian Catangiu
2023-02-17 11:45:00 +02:00
committed by GitHub
parent 36480b158d
commit c21f292a02
22 changed files with 2141 additions and 214 deletions
+131 -75
View File
@@ -34,10 +34,14 @@
mod commitment;
pub mod mmr;
mod payload;
#[cfg(feature = "std")]
mod test_utils;
pub mod witness;
pub use commitment::{Commitment, SignedCommitment, VersionedFinalityProof};
pub use payload::{known_payloads, BeefyPayloadId, Payload, PayloadProvider};
#[cfg(feature = "std")]
pub use test_utils::*;
use codec::{Codec, Decode, Encode};
use scale_info::TypeInfo;
@@ -183,6 +187,83 @@ pub struct VoteMessage<Number, Id, Signature> {
pub signature: Signature,
}
/// Proof of voter misbehavior on a given set id. Misbehavior/equivocation in
/// BEEFY happens when a voter votes on the same round/block for different payloads.
/// Proving is achieved by collecting the signed commitments of conflicting votes.
#[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)]
pub struct EquivocationProof<Number, Id, Signature> {
/// The first vote in the equivocation.
pub first: VoteMessage<Number, Id, Signature>,
/// The second vote in the equivocation.
pub second: VoteMessage<Number, Id, Signature>,
}
impl<Number, Id, Signature> EquivocationProof<Number, Id, Signature> {
/// Returns the authority id of the equivocator.
pub fn offender_id(&self) -> &Id {
&self.first.id
}
/// Returns the round number at which the equivocation occurred.
pub fn round_number(&self) -> &Number {
&self.first.commitment.block_number
}
/// Returns the set id at which the equivocation occurred.
pub fn set_id(&self) -> ValidatorSetId {
self.first.commitment.validator_set_id
}
}
/// Check a commitment signature by encoding the commitment and
/// verifying the provided signature using the expected authority id.
pub fn check_commitment_signature<Number, Id, MsgHash>(
commitment: &Commitment<Number>,
authority_id: &Id,
signature: &<Id as RuntimeAppPublic>::Signature,
) -> bool
where
Id: BeefyAuthorityId<MsgHash>,
Number: Clone + Encode + PartialEq,
MsgHash: Hash,
{
let encoded_commitment = commitment.encode();
BeefyAuthorityId::<MsgHash>::verify(authority_id, signature, &encoded_commitment)
}
/// Verifies the equivocation proof by making sure that both votes target
/// different blocks and that its signatures are valid.
pub fn check_equivocation_proof<Number, Id, MsgHash>(
report: &EquivocationProof<Number, Id, <Id as RuntimeAppPublic>::Signature>,
) -> bool
where
Id: BeefyAuthorityId<MsgHash> + PartialEq,
Number: Clone + Encode + PartialEq,
MsgHash: Hash,
{
let first = &report.first;
let second = &report.second;
// if votes
// come from different authorities,
// are for different rounds,
// have different validator set ids,
// or both votes have the same commitment,
// --> the equivocation is invalid.
if first.id != second.id ||
first.commitment.block_number != second.commitment.block_number ||
first.commitment.validator_set_id != second.commitment.validator_set_id ||
first.commitment.payload == second.commitment.payload
{
return false
}
// check signatures on both votes are valid
let valid_first = check_commitment_signature(&first.commitment, &first.id, &first.signature);
let valid_second =
check_commitment_signature(&second.commitment, &second.id, &second.signature);
return valid_first && valid_second
}
/// New BEEFY validator set notification hook.
pub trait OnNewValidatorSet<AuthorityId> {
/// Function called by the pallet when BEEFY validator set changes.
@@ -197,6 +278,28 @@ impl<AuthorityId> OnNewValidatorSet<AuthorityId> for () {
fn on_new_validator_set(_: &ValidatorSet<AuthorityId>, _: &ValidatorSet<AuthorityId>) {}
}
/// An opaque type used to represent the key ownership proof at the runtime API
/// boundary. The inner value is an encoded representation of the actual key
/// ownership proof which will be parameterized when defining the runtime. At
/// the runtime API boundary this type is unknown and as such we keep this
/// opaque representation, implementors of the runtime API will have to make
/// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type.
#[derive(Decode, Encode, PartialEq)]
pub struct OpaqueKeyOwnershipProof(Vec<u8>);
impl OpaqueKeyOwnershipProof {
/// Create a new `OpaqueKeyOwnershipProof` using the given encoded
/// representation.
pub fn new(inner: Vec<u8>) -> OpaqueKeyOwnershipProof {
OpaqueKeyOwnershipProof(inner)
}
/// Try to decode this `OpaqueKeyOwnershipProof` into the given concrete key
/// ownership proof type.
pub fn decode<T: Decode>(self) -> Option<T> {
codec::Decode::decode(&mut &self.0[..]).ok()
}
}
sp_api::decl_runtime_apis! {
/// API necessary for BEEFY voters.
pub trait BeefyApi
@@ -206,83 +309,36 @@ sp_api::decl_runtime_apis! {
/// Return the current active BEEFY validator set
fn validator_set() -> Option<ValidatorSet<crypto::AuthorityId>>;
}
}
#[cfg(feature = "std")]
/// Test accounts using [`crate::crypto`] types.
pub mod keyring {
use super::*;
use sp_core::{ecdsa, keccak_256, Pair};
use std::collections::HashMap;
use strum::IntoEnumIterator;
/// Submits an unsigned extrinsic to report an equivocation. The caller
/// must provide the equivocation proof and a key ownership proof
/// (should be obtained using `generate_key_ownership_proof`). The
/// extrinsic will be unsigned and should only be accepted for local
/// authorship (not to be broadcast to the network). This method returns
/// `None` when creation of the extrinsic fails, e.g. if equivocation
/// reporting is disabled for the given runtime (i.e. this method is
/// hardcoded to return `None`). Only useful in an offchain context.
fn submit_report_equivocation_unsigned_extrinsic(
equivocation_proof:
EquivocationProof<NumberFor<Block>, crypto::AuthorityId, crypto::Signature>,
key_owner_proof: OpaqueKeyOwnershipProof,
) -> Option<()>;
/// Set of test accounts using [`crate::crypto`] types.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumIter)]
pub enum Keyring {
Alice,
Bob,
Charlie,
Dave,
Eve,
Ferdie,
One,
Two,
}
impl Keyring {
/// Sign `msg`.
pub fn sign(self, msg: &[u8]) -> crypto::Signature {
// todo: use custom signature hashing type
let msg = keccak_256(msg);
ecdsa::Pair::from(self).sign_prehashed(&msg).into()
}
/// Return key pair.
pub fn pair(self) -> crypto::Pair {
ecdsa::Pair::from_string(self.to_seed().as_str(), None).unwrap().into()
}
/// Return public key.
pub fn public(self) -> crypto::Public {
self.pair().public()
}
/// Return seed string.
pub fn to_seed(self) -> String {
format!("//{}", self)
}
/// Get Keyring from public key.
pub fn from_public(who: &crypto::Public) -> Option<Keyring> {
Self::iter().find(|&k| &crypto::Public::from(k) == who)
}
}
lazy_static::lazy_static! {
static ref PRIVATE_KEYS: HashMap<Keyring, crypto::Pair> =
Keyring::iter().map(|i| (i, i.pair())).collect();
static ref PUBLIC_KEYS: HashMap<Keyring, crypto::Public> =
PRIVATE_KEYS.iter().map(|(&name, pair)| (name, pair.public())).collect();
}
impl From<Keyring> for crypto::Pair {
fn from(k: Keyring) -> Self {
k.pair()
}
}
impl From<Keyring> for ecdsa::Pair {
fn from(k: Keyring) -> Self {
k.pair().into()
}
}
impl From<Keyring> for crypto::Public {
fn from(k: Keyring) -> Self {
(*PUBLIC_KEYS).get(&k).cloned().unwrap()
}
/// Generates a proof of key ownership for the given authority in the
/// given set. An example usage of this module is coupled with the
/// session historical module to prove that a given authority key is
/// tied to a given staking identity during a specific session. Proofs
/// of key ownership are necessary for submitting equivocation reports.
/// NOTE: even though the API takes a `set_id` as parameter the current
/// implementations ignores this parameter and instead relies on this
/// method being called at the correct block height, i.e. any point at
/// which the given set id is live on-chain. Future implementations will
/// instead use indexed data through an offchain worker, not requiring
/// older states to be available.
fn generate_key_ownership_proof(
set_id: ValidatorSetId,
authority_id: crypto::AuthorityId,
) -> Option<OpaqueKeyOwnershipProof>;
}
}
@@ -0,0 +1,110 @@
// This file is part of Substrate.
// Copyright (C) 2021-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.
#![cfg(feature = "std")]
use crate::{crypto, Commitment, EquivocationProof, Payload, ValidatorSetId, VoteMessage};
use codec::Encode;
use sp_core::{ecdsa, keccak_256, Pair};
use std::collections::HashMap;
use strum::IntoEnumIterator;
/// Set of test accounts using [`crate::crypto`] types.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumIter)]
pub enum Keyring {
Alice,
Bob,
Charlie,
Dave,
Eve,
Ferdie,
One,
Two,
}
impl Keyring {
/// Sign `msg`.
pub fn sign(self, msg: &[u8]) -> crypto::Signature {
// todo: use custom signature hashing type
let msg = keccak_256(msg);
ecdsa::Pair::from(self).sign_prehashed(&msg).into()
}
/// Return key pair.
pub fn pair(self) -> crypto::Pair {
ecdsa::Pair::from_string(self.to_seed().as_str(), None).unwrap().into()
}
/// Return public key.
pub fn public(self) -> crypto::Public {
self.pair().public()
}
/// Return seed string.
pub fn to_seed(self) -> String {
format!("//{}", self)
}
/// Get Keyring from public key.
pub fn from_public(who: &crypto::Public) -> Option<Keyring> {
Self::iter().find(|&k| &crypto::Public::from(k) == who)
}
}
lazy_static::lazy_static! {
static ref PRIVATE_KEYS: HashMap<Keyring, crypto::Pair> =
Keyring::iter().map(|i| (i, i.pair())).collect();
static ref PUBLIC_KEYS: HashMap<Keyring, crypto::Public> =
PRIVATE_KEYS.iter().map(|(&name, pair)| (name, pair.public())).collect();
}
impl From<Keyring> for crypto::Pair {
fn from(k: Keyring) -> Self {
k.pair()
}
}
impl From<Keyring> for ecdsa::Pair {
fn from(k: Keyring) -> Self {
k.pair().into()
}
}
impl From<Keyring> for crypto::Public {
fn from(k: Keyring) -> Self {
(*PUBLIC_KEYS).get(&k).cloned().unwrap()
}
}
/// Create a new `EquivocationProof` based on given arguments.
pub fn generate_equivocation_proof(
vote1: (u64, Payload, ValidatorSetId, &Keyring),
vote2: (u64, Payload, ValidatorSetId, &Keyring),
) -> EquivocationProof<u64, crypto::Public, crypto::Signature> {
let signed_vote = |block_number: u64,
payload: Payload,
validator_set_id: ValidatorSetId,
keyring: &Keyring| {
let commitment = Commitment { validator_set_id, block_number, payload };
let signature = keyring.sign(&commitment.encode());
VoteMessage { commitment, id: keyring.public(), signature }
};
let first = signed_vote(vote1.0, vote1.1, vote1.2, vote1.3);
let second = signed_vote(vote2.0, vote2.1, vote2.2, vote2.3);
EquivocationProof { first, second }
}