mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-16 02:41:05 +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:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user