babe: report equivocations (#6362)

* slots: create primitives crate for consensus slots

* offences: add method to check if an offence is unknown

* babe: initial equivocation reporting implementation

* babe: organize imports

* babe: working equivocation reporting

* babe: add slot number to equivocation proof

* session: move duplicate traits to session primitives

* babe: move equivocation stuff to its own file

* offences: fix test

* session: don't have primitives depend on frame_support

* babe: use opaque type for key owner proof

* babe: cleanup client equivocation reporting

* babe: cleanup equivocation code in pallet

* babe: allow sending signed equivocation reports

* node: fix compilation

* fix test compilation

* babe: return bool on check_equivocation_proof

* babe: add test for equivocation reporting

* babe: add more tests

* babe: add test for validate unsigned

* babe: take slot number in generate_key_ownership_proof API

* babe: add benchmark for equivocation proof checking

* session: add benchmark for membership proof checking

* offences: fix babe benchmark

* babe: add weights based on benchmark results

* babe: adjust weights after benchmarking on reference hardware

* babe: reorder checks in check_and_report_equivocation
This commit is contained in:
André Silva
2020-07-04 11:18:13 +01:00
committed by GitHub
parent 61635e75c1
commit a9c21b8b84
34 changed files with 2031 additions and 275 deletions
@@ -17,9 +17,10 @@ codec = { package = "parity-scale-codec", version = "1.3.1", default-features =
merlin = { version = "2.0", default-features = false }
sp-std = { version = "2.0.0-rc4", default-features = false, path = "../../std" }
sp-api = { version = "2.0.0-rc4", default-features = false, path = "../../api" }
sp-core = { version = "2.0.0-rc4", default-features = false, path = "../../core" }
sp-consensus = { version = "0.8.0-rc4", optional = true, path = "../common" }
sp-consensus-slots = { version = "0.8.0-rc4", default-features = false, path = "../slots" }
sp-consensus-vrf = { version = "0.8.0-rc4", path = "../vrf", default-features = false }
sp-core = { version = "2.0.0-rc4", default-features = false, path = "../../core" }
sp-inherents = { version = "2.0.0-rc4", default-features = false, path = "../../inherents" }
sp-runtime = { version = "2.0.0-rc4", default-features = false, path = "../../runtime" }
sp-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../../timestamp" }
@@ -27,14 +28,15 @@ sp-timestamp = { version = "2.0.0-rc4", default-features = false, path = "../../
[features]
default = ["std"]
std = [
"sp-core/std",
"sp-application-crypto/std",
"codec/std",
"merlin/std",
"sp-std/std",
"sp-api/std",
"sp-consensus",
"sp-consensus-slots/std",
"sp-consensus-vrf/std",
"sp-core/std",
"sp-inherents/std",
"sp-runtime/std",
"sp-timestamp/std",
@@ -17,18 +17,14 @@
//! Private implementation details of BABE digests.
#[cfg(feature = "std")]
use super::{BABE_ENGINE_ID, AuthoritySignature};
use super::{AuthorityId, AuthorityIndex, SlotNumber, BabeAuthorityWeight, BabeEpochConfiguration, AllowedSlots};
#[cfg(feature = "std")]
use sp_runtime::{DigestItem, generic::OpaqueDigestItemId};
#[cfg(feature = "std")]
use std::fmt::Debug;
use codec::{Decode, Encode};
#[cfg(feature = "std")]
use codec::Codec;
use super::{
AllowedSlots, AuthorityId, AuthorityIndex, AuthoritySignature, BabeAuthorityWeight,
BabeEpochConfiguration, SlotNumber, BABE_ENGINE_ID,
};
use codec::{Codec, Decode, Encode};
use sp_std::vec::Vec;
use sp_runtime::RuntimeDebug;
use sp_runtime::{generic::OpaqueDigestItemId, DigestItem, RuntimeDebug};
use sp_consensus_vrf::schnorrkel::{Randomness, VRFOutput, VRFProof};
/// Raw BABE primary slot assignment pre-digest.
@@ -151,7 +147,6 @@ impl From<NextConfigDescriptor> for BabeEpochConfiguration {
}
/// A digest item which is usable with BABE consensus.
#[cfg(feature = "std")]
pub trait CompatibleDigestItem: Sized {
/// Construct a digest item which contains a BABE pre-digest.
fn babe_pre_digest(seal: PreDigest) -> Self;
@@ -172,9 +167,8 @@ pub trait CompatibleDigestItem: Sized {
fn as_next_config_descriptor(&self) -> Option<NextConfigDescriptor>;
}
#[cfg(feature = "std")]
impl<Hash> CompatibleDigestItem for DigestItem<Hash> where
Hash: Debug + Send + Sync + Eq + Clone + Codec + 'static
Hash: Send + Sync + Eq + Clone + Codec + 'static
{
fn babe_pre_digest(digest: PreDigest) -> Self {
DigestItem::PreRuntime(BABE_ENGINE_ID, digest.encode())
+131 -8
View File
@@ -23,17 +23,21 @@
pub mod digests;
pub mod inherents;
pub use sp_consensus_vrf::schnorrkel::{
Randomness, VRF_PROOF_LENGTH, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH
};
pub use merlin::Transcript;
pub use sp_consensus_vrf::schnorrkel::{
Randomness, RANDOMNESS_LENGTH, VRF_OUTPUT_LENGTH, VRF_PROOF_LENGTH,
};
use codec::{Encode, Decode};
use sp_std::vec::Vec;
use sp_runtime::{ConsensusEngineId, RuntimeDebug};
use codec::{Decode, Encode};
#[cfg(feature = "std")]
use sp_core::vrf::{VRFTranscriptData, VRFTranscriptValue};
use crate::digests::{NextEpochDescriptor, NextConfigDescriptor};
use sp_runtime::{traits::Header, ConsensusEngineId, RuntimeDebug};
use sp_std::vec::Vec;
use crate::digests::{NextConfigDescriptor, NextEpochDescriptor};
/// Key type for BABE module.
pub const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_application_crypto::key_types::BABE;
mod app {
use sp_application_crypto::{app_crypto, key_types::BABE, sr25519};
@@ -73,7 +77,10 @@ pub const MEDIAN_ALGORITHM_CARDINALITY: usize = 1200; // arbitrary suggestion by
pub type AuthorityIndex = u32;
/// A slot number.
pub type SlotNumber = u64;
pub use sp_consensus_slots::SlotNumber;
/// An equivocation proof for multiple block authorships on the same slot (i.e. double vote).
pub type EquivocationProof<H> = sp_consensus_slots::EquivocationProof<H, AuthorityId>;
/// The weight of an authority.
// NOTE: we use a unique name for the weight to avoid conflicts with other
@@ -256,6 +263,93 @@ pub struct BabeEpochConfiguration {
pub allowed_slots: AllowedSlots,
}
/// Verifies the equivocation proof by making sure that: both headers have
/// different hashes, are targetting the same slot, and have valid signatures by
/// the same authority.
pub fn check_equivocation_proof<H>(proof: EquivocationProof<H>) -> bool
where
H: Header,
{
use digests::*;
use sp_application_crypto::RuntimeAppPublic;
let find_pre_digest = |header: &H| {
header
.digest()
.logs()
.iter()
.find_map(|log| log.as_babe_pre_digest())
};
let verify_seal_signature = |mut header: H, offender: &AuthorityId| {
let seal = header.digest_mut().pop()?.as_babe_seal()?;
let pre_hash = header.hash();
if !offender.verify(&pre_hash.as_ref(), &seal) {
return None;
}
Some(())
};
let verify_proof = || {
// we must have different headers for the equivocation to be valid
if proof.first_header.hash() == proof.second_header.hash() {
return None;
}
let first_pre_digest = find_pre_digest(&proof.first_header)?;
let second_pre_digest = find_pre_digest(&proof.second_header)?;
// both headers must be targetting the same slot and it must
// be the same as the one in the proof.
if proof.slot_number != first_pre_digest.slot_number() ||
first_pre_digest.slot_number() != second_pre_digest.slot_number()
{
return None;
}
// both headers must have been authored by the same authority
if first_pre_digest.authority_index() != second_pre_digest.authority_index() {
return None;
}
// we finally verify that the expected authority has signed both headers and
// that the signature is valid.
verify_seal_signature(proof.first_header, &proof.offender)?;
verify_seal_signature(proof.second_header, &proof.offender)?;
Some(())
};
// NOTE: we isolate the verification code into an helper function that
// returns `Option<()>` so that we can use `?` to deal with any intermediate
// errors and discard the proof as invalid.
verify_proof().is_some()
}
/// 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> {
Decode::decode(&mut &self.0[..]).ok()
}
}
sp_api::decl_runtime_apis! {
/// API necessary for block authorship with BABE.
#[api_version(2)]
@@ -269,5 +363,34 @@ sp_api::decl_runtime_apis! {
/// Returns the slot number that started the current epoch.
fn current_epoch_start() -> SlotNumber;
/// Generates a proof of key ownership for the given authority in the
/// current epoch. 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 `slot_number` 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 epoch for the given slot 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(
slot_number: SlotNumber,
authority_id: AuthorityId,
) -> Option<OpaqueKeyOwnershipProof>;
/// 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<Block::Header>,
key_owner_proof: OpaqueKeyOwnershipProof,
) -> Option<()>;
}
}
@@ -0,0 +1,23 @@
[package]
name = "sp-consensus-slots"
version = "0.8.0-rc4"
authors = ["Parity Technologies <admin@parity.io>"]
description = "Primitives for slots-based consensus"
edition = "2018"
license = "Apache-2.0"
homepage = "https://substrate.dev"
repository = "https://github.com/paritytech/substrate/"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] }
sp-runtime = { version = "2.0.0-rc2", default-features = false, path = "../../runtime" }
[features]
default = ["std"]
std = [
"codec/std",
"sp-runtime/std",
]
@@ -0,0 +1,41 @@
// This file is part of Substrate.
// Copyright (C) 2020 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.
//! Primitives for slots-based consensus engines.
#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode};
/// A slot number.
pub type SlotNumber = u64;
/// Represents an equivocation proof. An equivocation happens when a validator
/// produces more than one block on the same slot. The proof of equivocation
/// are the given distinct headers that were signed by the validator and which
/// include the slot number.
#[derive(Clone, Debug, Decode, Encode, PartialEq)]
pub struct EquivocationProof<Header, Id> {
/// Returns the authority id of the equivocator.
pub offender: Id,
/// The slot number at which the equivocation happened.
pub slot_number: SlotNumber,
/// The first header involved in the equivocation.
pub first_header: Header,
/// The second header involved in the equivocation.
pub second_header: Header,
}
+5
View File
@@ -333,6 +333,11 @@ pub fn to_substrate_wasm_fn_return_value(value: &impl Encode) -> u64 {
res
}
/// The void type - it cannot exist.
// Oh rust, you crack me up...
#[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug)]
pub enum Void {}
/// Macro for creating `Maybe*` marker traits.
///
/// Such a maybe-marker trait requires the given bound when `feature = std` and doesn't require
+42
View File
@@ -64,6 +64,48 @@ pub struct MembershipProof {
pub validator_count: ValidatorCount,
}
/// A utility trait to get a session number. This is implemented for
/// `MembershipProof` below to fetch the session number the given session
/// membership proof is for. It is useful when we need to deal with key owner
/// proofs generically (i.e. just typing against the `KeyOwnerProofSystem`
/// trait) but still restrict their capabilities.
pub trait GetSessionNumber {
fn session(&self) -> SessionIndex;
}
/// A utility trait to get the validator count of a given session. This is
/// implemented for `MembershipProof` below and fetches the number of validators
/// in the session the membership proof is for. It is useful when we need to
/// deal with key owner proofs generically (i.e. just typing against the
/// `KeyOwnerProofSystem` trait) but still restrict their capabilities.
pub trait GetValidatorCount {
fn validator_count(&self) -> ValidatorCount;
}
impl GetSessionNumber for sp_core::Void {
fn session(&self) -> SessionIndex {
Default::default()
}
}
impl GetValidatorCount for sp_core::Void {
fn validator_count(&self) -> ValidatorCount {
Default::default()
}
}
impl GetSessionNumber for MembershipProof {
fn session(&self) -> SessionIndex {
self.session
}
}
impl GetValidatorCount for MembershipProof {
fn validator_count(&self) -> ValidatorCount {
self.validator_count
}
}
/// Generate the initial session keys with the given seeds, at the given block and store them in
/// the client's keystore.
#[cfg(feature = "std")]
+12 -1
View File
@@ -117,10 +117,21 @@ impl sp_runtime::traits::Printable for OffenceError {
pub trait ReportOffence<Reporter, Offender, O: Offence<Offender>> {
/// Report an `offence` and reward given `reporters`.
fn report_offence(reporters: Vec<Reporter>, offence: O) -> Result<(), OffenceError>;
/// Returns true iff all of the given offenders have been previously reported
/// at the given time slot. This function is useful to prevent the sending of
/// duplicate offence reports.
fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool;
}
impl<Reporter, Offender, O: Offence<Offender>> ReportOffence<Reporter, Offender, O> for () {
fn report_offence(_reporters: Vec<Reporter>, _offence: O) -> Result<(), OffenceError> { Ok(()) }
fn report_offence(_reporters: Vec<Reporter>, _offence: O) -> Result<(), OffenceError> {
Ok(())
}
fn is_known_offence(_offenders: &[Offender], _time_slot: &O::TimeSlot) -> bool {
true
}
}
/// A trait to take action on an offence.