mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-13 03:31:10 +00:00
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:
@@ -0,0 +1,108 @@
|
||||
// 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.
|
||||
|
||||
//! Benchmarks for the BABE Pallet.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use super::*;
|
||||
use frame_benchmarking::benchmarks;
|
||||
|
||||
type Header = sp_runtime::generic::Header<u64, sp_runtime::traits::BlakeTwo256>;
|
||||
|
||||
benchmarks! {
|
||||
_ { }
|
||||
|
||||
check_equivocation_proof {
|
||||
let x in 0 .. 1;
|
||||
|
||||
// NOTE: generated with the test below `test_generate_equivocation_report_blob`.
|
||||
// the output is not deterministic since keys are generated randomly (and therefore
|
||||
// signature content changes). it should not affect the benchmark.
|
||||
// with the current benchmark setup it is not possible to generate this programatically
|
||||
// from the benchmark setup.
|
||||
const EQUIVOCATION_PROOF_BLOB: [u8; 416] = [
|
||||
222, 241, 46, 66, 243, 228, 135, 233, 177, 64, 149, 170, 141, 92, 193, 106, 51, 73, 31,
|
||||
27, 80, 218, 220, 248, 129, 29, 20, 128, 243, 250, 134, 39, 11, 0, 0, 0, 0, 0, 0, 0,
|
||||
158, 4, 7, 240, 67, 153, 134, 190, 251, 196, 229, 95, 136, 165, 234, 228, 255, 18, 2,
|
||||
187, 76, 125, 108, 50, 67, 33, 196, 108, 38, 115, 179, 86, 40, 36, 27, 5, 105, 58, 228,
|
||||
94, 198, 65, 212, 218, 213, 61, 170, 21, 51, 249, 182, 121, 101, 91, 204, 25, 31, 87,
|
||||
219, 208, 43, 119, 211, 185, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 66, 65, 66, 69, 52, 2, 0, 0, 0, 0, 11,
|
||||
0, 0, 0, 0, 0, 0, 0, 5, 66, 65, 66, 69, 1, 1, 188, 192, 217, 91, 138, 78, 217, 80, 8,
|
||||
29, 140, 55, 242, 210, 170, 184, 73, 98, 135, 212, 236, 209, 115, 52, 200, 79, 175,
|
||||
172, 242, 161, 199, 47, 236, 93, 101, 95, 43, 34, 141, 16, 247, 220, 33, 59, 31, 197,
|
||||
27, 7, 196, 62, 12, 238, 236, 124, 136, 191, 29, 36, 22, 238, 242, 202, 57, 139, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 40, 23, 175, 153, 83, 6, 33, 65, 123, 51, 80, 223, 126, 186, 226, 225, 240, 105, 28,
|
||||
169, 9, 54, 11, 138, 46, 194, 201, 250, 48, 242, 125, 117, 116, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 66, 65,
|
||||
66, 69, 52, 2, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 5, 66, 65, 66, 69, 1, 1, 142, 12,
|
||||
124, 11, 167, 227, 103, 88, 78, 23, 228, 33, 96, 41, 207, 183, 227, 189, 114, 70, 254,
|
||||
30, 128, 243, 233, 83, 214, 45, 74, 182, 120, 119, 64, 243, 219, 119, 63, 240, 205,
|
||||
123, 231, 82, 205, 174, 143, 70, 2, 86, 182, 20, 16, 141, 145, 91, 116, 195, 58, 223,
|
||||
175, 145, 255, 7, 121, 133
|
||||
];
|
||||
|
||||
let equivocation_proof1: sp_consensus_babe::EquivocationProof<Header> =
|
||||
Decode::decode(&mut &EQUIVOCATION_PROOF_BLOB[..]).unwrap();
|
||||
|
||||
let equivocation_proof2 = equivocation_proof1.clone();
|
||||
}: {
|
||||
sp_consensus_babe::check_equivocation_proof::<Header>(equivocation_proof1);
|
||||
} verify {
|
||||
assert!(sp_consensus_babe::check_equivocation_proof::<Header>(equivocation_proof2));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::*;
|
||||
use frame_support::assert_ok;
|
||||
|
||||
#[test]
|
||||
fn test_benchmarks() {
|
||||
new_test_ext(3).execute_with(|| {
|
||||
assert_ok!(test_benchmark_check_equivocation_proof::<Test>());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_equivocation_report_blob() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
let offending_authority_index = 0;
|
||||
let offending_authority_pair = &pairs[0];
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_authority_index,
|
||||
offending_authority_pair,
|
||||
CurrentSlot::get() + 1,
|
||||
);
|
||||
|
||||
println!("equivocation_proof: {:?}", equivocation_proof);
|
||||
println!(
|
||||
"equivocation_proof.encode(): {:?}",
|
||||
equivocation_proof.encode()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// 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.
|
||||
|
||||
//!
|
||||
//! An opt-in utility module for reporting equivocations.
|
||||
//!
|
||||
//! This module defines an offence type for BABE equivocations
|
||||
//! and some utility traits to wire together:
|
||||
//! - a system for reporting offences;
|
||||
//! - a system for submitting unsigned 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 import BABE blocks).
|
||||
//! And in a runtime context, so that the BABE 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 BABE pallet is used in the runtime
|
||||
//! definition.
|
||||
//!
|
||||
|
||||
use frame_support::{debug, traits::KeyOwnerProofSystem};
|
||||
use sp_consensus_babe::{EquivocationProof, SlotNumber};
|
||||
use sp_runtime::transaction_validity::{
|
||||
InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity,
|
||||
TransactionValidityError, ValidTransaction,
|
||||
};
|
||||
use sp_runtime::{DispatchResult, Perbill};
|
||||
use sp_staking::{
|
||||
offence::{Kind, Offence, OffenceError, ReportOffence},
|
||||
SessionIndex,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use crate::{Call, Module, Trait};
|
||||
|
||||
/// A trait with utility methods for handling equivocation reports in BABE.
|
||||
/// The trait provides methods for reporting an offence triggered by a valid
|
||||
/// equivocation report, checking the current block author (to declare as the
|
||||
/// reporter), and also for creating and submitting equivocation report
|
||||
/// extrinsics (useful only in offchain context).
|
||||
pub trait HandleEquivocation<T: Trait> {
|
||||
/// Report an offence proved by the given reporters.
|
||||
fn report_offence(
|
||||
reporters: Vec<T::AccountId>,
|
||||
offence: BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> 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: &SlotNumber) -> bool;
|
||||
|
||||
/// Create and dispatch an equivocation report extrinsic.
|
||||
fn submit_unsigned_equivocation_report(
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResult;
|
||||
|
||||
/// Fetch the current block author id, if defined.
|
||||
fn block_author() -> Option<T::AccountId>;
|
||||
}
|
||||
|
||||
impl<T: Trait> HandleEquivocation<T> for () {
|
||||
fn report_offence(
|
||||
_reporters: Vec<T::AccountId>,
|
||||
_offence: BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_known_offence(_offenders: &[T::KeyOwnerIdentification], _time_slot: &SlotNumber) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn submit_unsigned_equivocation_report(
|
||||
_equivocation_proof: EquivocationProof<T::Header>,
|
||||
_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<I, R> {
|
||||
_phantom: sp_std::marker::PhantomData<(I, R)>,
|
||||
}
|
||||
|
||||
impl<I, R> Default for EquivocationHandler<I, R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> HandleEquivocation<T> for EquivocationHandler<T::KeyOwnerIdentification, R>
|
||||
where
|
||||
// We use the authorship pallet to fetch the current block author and use
|
||||
// `offchain::SendTransactionTypes` for unsigned extrinsic creation and
|
||||
// submission.
|
||||
T: Trait + pallet_authorship::Trait + frame_system::offchain::SendTransactionTypes<Call<T>>,
|
||||
// A system for reporting offences after valid equivocation reports are
|
||||
// processed.
|
||||
R: ReportOffence<
|
||||
T::AccountId,
|
||||
T::KeyOwnerIdentification,
|
||||
BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
>,
|
||||
{
|
||||
fn report_offence(
|
||||
reporters: Vec<T::AccountId>,
|
||||
offence: BabeEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
R::report_offence(reporters, offence)
|
||||
}
|
||||
|
||||
fn is_known_offence(offenders: &[T::KeyOwnerIdentification], time_slot: &SlotNumber) -> bool {
|
||||
R::is_known_offence(offenders, time_slot)
|
||||
}
|
||||
|
||||
fn submit_unsigned_equivocation_report(
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResult {
|
||||
use frame_system::offchain::SubmitTransaction;
|
||||
|
||||
let call = Call::report_equivocation_unsigned(equivocation_proof, key_owner_proof);
|
||||
|
||||
match SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) {
|
||||
Ok(()) => debug::info!("Submitted BABE equivocation report."),
|
||||
Err(e) => debug::error!("Error submitting equivocation report: {:?}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn block_author() -> Option<T::AccountId> {
|
||||
Some(<pallet_authorship::Module<T>>::author())
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ValidateUnsigned` implementation that 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: Trait> frame_support::unsigned::ValidateUnsigned for Module<T> {
|
||||
type Call = Call<T>;
|
||||
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
|
||||
if let Call::report_equivocation_unsigned(equivocation_proof, _) = call {
|
||||
// discard equivocation report not coming from the local node
|
||||
match source {
|
||||
TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }
|
||||
_ => {
|
||||
debug::warn!(
|
||||
target: "babe",
|
||||
"rejecting unsigned report equivocation transaction because it is not local/in-block."
|
||||
);
|
||||
|
||||
return InvalidTransaction::Call.into();
|
||||
}
|
||||
}
|
||||
|
||||
ValidTransaction::with_tag_prefix("BabeEquivocation")
|
||||
// We assign the maximum priority for any equivocation report.
|
||||
.priority(TransactionPriority::max_value())
|
||||
// Only one equivocation report for the same offender at the same slot.
|
||||
.and_provides((
|
||||
equivocation_proof.offender.clone(),
|
||||
equivocation_proof.slot_number,
|
||||
))
|
||||
// We don't propagate this. This can never be included on a remote node.
|
||||
.propagate(false)
|
||||
.build()
|
||||
} else {
|
||||
InvalidTransaction::Call.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
|
||||
if let Call::report_equivocation_unsigned(equivocation_proof, key_owner_proof) = call {
|
||||
// check the membership proof to extract the offender's id
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
equivocation_proof.offender.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 is_known_offence = T::HandleEquivocation::is_known_offence(
|
||||
&[offender],
|
||||
&equivocation_proof.slot_number,
|
||||
);
|
||||
|
||||
if is_known_offence {
|
||||
Err(InvalidTransaction::Stale.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Err(InvalidTransaction::Call.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A BABE equivocation offence report.
|
||||
///
|
||||
/// When a validator released two or more blocks at the same slot.
|
||||
pub struct BabeEquivocationOffence<FullIdentification> {
|
||||
/// A babe slot number in which this incident happened.
|
||||
pub slot: SlotNumber,
|
||||
/// 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 that produced the equivocation.
|
||||
pub offender: FullIdentification,
|
||||
}
|
||||
|
||||
impl<FullIdentification: Clone> Offence<FullIdentification>
|
||||
for BabeEquivocationOffence<FullIdentification>
|
||||
{
|
||||
const ID: Kind = *b"babe:equivocatio";
|
||||
type TimeSlot = SlotNumber;
|
||||
|
||||
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.slot
|
||||
}
|
||||
|
||||
fn slash_fraction(offenders_count: u32, validator_set_count: u32) -> Perbill {
|
||||
// the formula is min((3k / n)^2, 1)
|
||||
let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count);
|
||||
// _ ^ 2
|
||||
x.square()
|
||||
}
|
||||
}
|
||||
+187
-64
@@ -21,37 +21,44 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![warn(unused_must_use, unsafe_code, unused_variables, unused_must_use)]
|
||||
|
||||
use pallet_timestamp;
|
||||
|
||||
use sp_std::{result, prelude::*};
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
decl_storage, decl_module, traits::{FindAuthor, Get, Randomness as RandomnessT},
|
||||
decl_error, decl_module, decl_storage,
|
||||
traits::{FindAuthor, Get, KeyOwnerProofSystem, Randomness as RandomnessT},
|
||||
weights::Weight,
|
||||
Parameter,
|
||||
};
|
||||
use sp_timestamp::OnTimestampSet;
|
||||
use sp_runtime::{generic::DigestItem, ConsensusEngineId, Perbill};
|
||||
use sp_runtime::traits::{IsMember, SaturatedConversion, Saturating, Hash, One};
|
||||
use sp_staking::{
|
||||
SessionIndex,
|
||||
offence::{Offence, Kind},
|
||||
};
|
||||
use frame_system::{ensure_none, ensure_signed};
|
||||
use sp_application_crypto::Public;
|
||||
use sp_runtime::{
|
||||
generic::DigestItem,
|
||||
traits::{Hash, IsMember, One, SaturatedConversion, Saturating},
|
||||
ConsensusEngineId, KeyTypeId,
|
||||
};
|
||||
use sp_session::{GetSessionNumber, GetValidatorCount};
|
||||
use sp_std::{prelude::*, result};
|
||||
use sp_timestamp::OnTimestampSet;
|
||||
|
||||
use codec::{Encode, Decode};
|
||||
use sp_inherents::{InherentIdentifier, InherentData, ProvideInherent, MakeFatalError};
|
||||
use sp_consensus_babe::{
|
||||
BABE_ENGINE_ID, ConsensusLog, BabeAuthorityWeight, SlotNumber,
|
||||
inherents::{INHERENT_IDENTIFIER, BabeInherentData},
|
||||
digests::{NextEpochDescriptor, NextConfigDescriptor, PreDigest},
|
||||
digests::{NextConfigDescriptor, NextEpochDescriptor, PreDigest},
|
||||
inherents::{BabeInherentData, INHERENT_IDENTIFIER},
|
||||
BabeAuthorityWeight, ConsensusLog, EquivocationProof, SlotNumber, BABE_ENGINE_ID,
|
||||
};
|
||||
use sp_consensus_vrf::schnorrkel;
|
||||
pub use sp_consensus_babe::{AuthorityId, VRF_OUTPUT_LENGTH, RANDOMNESS_LENGTH, PUBLIC_KEY_LENGTH};
|
||||
use sp_inherents::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent};
|
||||
|
||||
pub use sp_consensus_babe::{AuthorityId, PUBLIC_KEY_LENGTH, RANDOMNESS_LENGTH, VRF_OUTPUT_LENGTH};
|
||||
|
||||
mod equivocation;
|
||||
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
mod benchmarking;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod mock;
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod mock;
|
||||
pub use equivocation::{BabeEquivocationOffence, EquivocationHandler, HandleEquivocation};
|
||||
|
||||
pub trait Trait: pallet_timestamp::Trait {
|
||||
/// The amount of time, in slots, that each epoch should last.
|
||||
@@ -70,6 +77,30 @@ pub trait Trait: pallet_timestamp::Trait {
|
||||
/// Typically, the `ExternalTrigger` type should be used. An internal trigger should only be used
|
||||
/// when no other module is responsible for changing authority set.
|
||||
type EpochChangeTrigger: EpochChangeTrigger;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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, AuthorityId),
|
||||
Proof = Self::KeyOwnerProof,
|
||||
IdentificationTuple = Self::KeyOwnerIdentification,
|
||||
>;
|
||||
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
/// Trigger an epoch change, if any should take place.
|
||||
@@ -106,6 +137,17 @@ const UNDER_CONSTRUCTION_SEGMENT_LENGTH: usize = 256;
|
||||
|
||||
type MaybeRandomness = Option<schnorrkel::Randomness>;
|
||||
|
||||
decl_error! {
|
||||
pub enum Error for Module<T: Trait> {
|
||||
/// An equivocation proof provided as part of an equivocation report is invalid.
|
||||
InvalidEquivocationProof,
|
||||
/// A key ownership proof provided as part of an equivocation report is invalid.
|
||||
InvalidKeyOwnershipProof,
|
||||
/// A given equivocation report is valid but already previously reported.
|
||||
DuplicateOffenceReport,
|
||||
}
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Trait> as Babe {
|
||||
/// Current epoch index.
|
||||
@@ -208,6 +250,69 @@ decl_module! {
|
||||
// remove temporary "environment" entry from storage
|
||||
Lateness::<T>::kill();
|
||||
}
|
||||
|
||||
/// Report authority 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.
|
||||
#[weight = weight::weight_for_report_equivocation::<T>()]
|
||||
fn report_equivocation(
|
||||
origin,
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) {
|
||||
let reporter = ensure_signed(origin)?;
|
||||
|
||||
Self::do_report_equivocation(
|
||||
Some(reporter),
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)?;
|
||||
}
|
||||
|
||||
/// Report authority 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.
|
||||
#[weight = weight::weight_for_report_equivocation::<T>()]
|
||||
fn report_equivocation_unsigned(
|
||||
origin,
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) {
|
||||
ensure_none(origin)?;
|
||||
|
||||
Self::do_report_equivocation(
|
||||
T::HandleEquivocation::block_author(),
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod weight {
|
||||
use frame_support::{
|
||||
traits::Get,
|
||||
weights::{constants::WEIGHT_PER_MICROS, Weight},
|
||||
};
|
||||
|
||||
pub fn weight_for_report_equivocation<T: super::Trait>() -> Weight {
|
||||
// checking membership proof
|
||||
(35 * WEIGHT_PER_MICROS)
|
||||
.saturating_add(T::DbWeight::get().reads(5))
|
||||
// check equivocation proof
|
||||
.saturating_add(110 * WEIGHT_PER_MICROS)
|
||||
// report offence
|
||||
.saturating_add(110 * WEIGHT_PER_MICROS)
|
||||
// worst case we are considering is that the given offender
|
||||
// is backed by 200 nominators
|
||||
.saturating_add(T::DbWeight::get().reads(14 + 3 * 200))
|
||||
.saturating_add(T::DbWeight::get().writes(10 + 3 * 200))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,51 +379,6 @@ impl<T: Trait> pallet_session::ShouldEndSession<T::BlockNumber> for Module<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A BABE equivocation offence report.
|
||||
///
|
||||
/// When a validator released two or more blocks at the same slot.
|
||||
pub struct BabeEquivocationOffence<FullIdentification> {
|
||||
/// A babe slot number in which this incident happened.
|
||||
pub slot: u64,
|
||||
/// 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 that produced the equivocation.
|
||||
pub offender: FullIdentification,
|
||||
}
|
||||
|
||||
impl<FullIdentification: Clone> Offence<FullIdentification> for BabeEquivocationOffence<FullIdentification> {
|
||||
const ID: Kind = *b"babe:equivocatio";
|
||||
type TimeSlot = u64;
|
||||
|
||||
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.slot
|
||||
}
|
||||
|
||||
fn slash_fraction(
|
||||
offenders_count: u32,
|
||||
validator_set_count: u32,
|
||||
) -> Perbill {
|
||||
// the formula is min((3k / n)^2, 1)
|
||||
let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count);
|
||||
// _ ^ 2
|
||||
x.square()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
/// Determine the BABE slot duration based on the Timestamp module configuration.
|
||||
pub fn slot_duration() -> T::Moment {
|
||||
@@ -561,6 +621,69 @@ impl<T: Trait> Module<T> {
|
||||
Authorities::put(authorities);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_report_equivocation(
|
||||
reporter: Option<T::AccountId>,
|
||||
equivocation_proof: EquivocationProof<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Result<(), Error<T>> {
|
||||
let offender = equivocation_proof.offender.clone();
|
||||
let slot_number = equivocation_proof.slot_number;
|
||||
|
||||
// validate the equivocation proof
|
||||
if !sp_consensus_babe::check_equivocation_proof(equivocation_proof) {
|
||||
return Err(Error::InvalidEquivocationProof.into());
|
||||
}
|
||||
|
||||
let validator_set_count = key_owner_proof.validator_count();
|
||||
let session_index = key_owner_proof.session();
|
||||
|
||||
let epoch_index = (slot_number.saturating_sub(GenesisSlot::get()) / T::EpochDuration::get())
|
||||
.saturated_into::<u32>();
|
||||
|
||||
// check that the slot number is consistent with the session index
|
||||
// in the key ownership proof (i.e. slot is for that epoch)
|
||||
if epoch_index != session_index {
|
||||
return Err(Error::InvalidKeyOwnershipProof.into());
|
||||
}
|
||||
|
||||
// check the membership proof and extract the offender's id
|
||||
let key = (sp_consensus_babe::KEY_TYPE, offender);
|
||||
let offender = T::KeyOwnerProofSystem::check_proof(key, key_owner_proof)
|
||||
.ok_or(Error::InvalidKeyOwnershipProof)?;
|
||||
|
||||
let offence = BabeEquivocationOffence {
|
||||
slot: slot_number,
|
||||
validator_set_count,
|
||||
offender,
|
||||
session_index,
|
||||
};
|
||||
|
||||
let reporters = match reporter {
|
||||
Some(id) => vec![id],
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
T::HandleEquivocation::report_offence(reporters, offence)
|
||||
.map_err(|_| Error::DuplicateOffenceReport)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<T::Header>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Option<()> {
|
||||
T::HandleEquivocation::submit_unsigned_equivocation_report(
|
||||
equivocation_proof,
|
||||
key_owner_proof,
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> OnTimestampSet<T::Moment> for Module<T> {
|
||||
|
||||
@@ -18,27 +18,37 @@
|
||||
//! Test utilities
|
||||
|
||||
use codec::Encode;
|
||||
use super::{Trait, Module, GenesisConfig, CurrentSlot};
|
||||
use super::{Trait, Module, CurrentSlot};
|
||||
use sp_runtime::{
|
||||
Perbill, impl_opaque_keys,
|
||||
testing::{Header, UintAuthorityId, Digest, DigestItem},
|
||||
traits::IdentityLookup,
|
||||
curve::PiecewiseLinear,
|
||||
testing::{Digest, DigestItem, Header, TestXt,},
|
||||
traits::{Convert, Header as _, IdentityLookup, OpaqueKeys, SaturatedConversion},
|
||||
};
|
||||
use frame_system::InitKind;
|
||||
use frame_support::{
|
||||
impl_outer_origin, parameter_types, StorageValue,
|
||||
traits::OnInitialize,
|
||||
impl_outer_dispatch, impl_outer_origin, parameter_types, StorageValue,
|
||||
traits::{KeyOwnerProofSystem, OnInitialize},
|
||||
weights::Weight,
|
||||
};
|
||||
use sp_io;
|
||||
use sp_core::{H256, U256, crypto::Pair};
|
||||
use sp_consensus_babe::AuthorityPair;
|
||||
use sp_core::{H256, U256, crypto::{KeyTypeId, Pair}};
|
||||
use sp_consensus_babe::{AuthorityId, AuthorityPair, SlotNumber};
|
||||
use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof};
|
||||
use sp_staking::SessionIndex;
|
||||
use pallet_staking::EraIndex;
|
||||
|
||||
impl_outer_origin!{
|
||||
pub enum Origin for Test where system = frame_system {}
|
||||
}
|
||||
|
||||
impl_outer_dispatch! {
|
||||
pub enum Call for Test where origin: Origin {
|
||||
babe::Babe,
|
||||
staking::Staking,
|
||||
}
|
||||
}
|
||||
|
||||
type DummyValidatorId = u64;
|
||||
|
||||
// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
|
||||
@@ -50,7 +60,6 @@ parameter_types! {
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
pub const MinimumPeriod: u64 = 1;
|
||||
pub const EpochDuration: u64 = 3;
|
||||
pub const ExpectedBlockTime: u64 = 1;
|
||||
pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(16);
|
||||
@@ -61,7 +70,7 @@ impl frame_system::Trait for Test {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Call = ();
|
||||
type Call = Call;
|
||||
type Hash = H256;
|
||||
type Version = ();
|
||||
type Hashing = sp_runtime::traits::BlakeTwo256;
|
||||
@@ -78,27 +87,55 @@ impl frame_system::Trait for Test {
|
||||
type AvailableBlockRatio = AvailableBlockRatio;
|
||||
type MaximumBlockLength = MaximumBlockLength;
|
||||
type ModuleToIndex = ();
|
||||
type AccountData = ();
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
}
|
||||
|
||||
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
|
||||
where
|
||||
Call: From<C>,
|
||||
{
|
||||
type OverarchingCall = Call;
|
||||
type Extrinsic = TestXt<Call, ()>;
|
||||
}
|
||||
|
||||
impl_opaque_keys! {
|
||||
pub struct MockSessionKeys {
|
||||
pub dummy: UintAuthorityId,
|
||||
pub babe_authority: super::Module<Test>,
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_session::Trait for Test {
|
||||
type Event = ();
|
||||
type ValidatorId = <Self as frame_system::Trait>::AccountId;
|
||||
type ValidatorIdOf = pallet_staking::StashOf<Self>;
|
||||
type ShouldEndSession = Babe;
|
||||
type SessionHandler = (Babe,);
|
||||
type SessionManager = ();
|
||||
type ValidatorIdOf = ();
|
||||
type NextSessionRotation = Babe;
|
||||
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Self, Staking>;
|
||||
type SessionHandler = <MockSessionKeys as OpaqueKeys>::KeyTypeIdProviders;
|
||||
type Keys = MockSessionKeys;
|
||||
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
|
||||
type NextSessionRotation = Babe;
|
||||
}
|
||||
|
||||
impl pallet_session::historical::Trait for Test {
|
||||
type FullIdentification = pallet_staking::Exposure<u64, u128>;
|
||||
type FullIdentificationOf = pallet_staking::ExposureOf<Self>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const UncleGenerations: u64 = 0;
|
||||
}
|
||||
|
||||
impl pallet_authorship::Trait for Test {
|
||||
type FindAuthor = pallet_session::FindAccountFromAuthorIndex<Self, Babe>;
|
||||
type UncleGenerations = UncleGenerations;
|
||||
type FilterUncle = ();
|
||||
type EventHandler = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MinimumPeriod: u64 = 1;
|
||||
}
|
||||
|
||||
impl pallet_timestamp::Trait for Test {
|
||||
@@ -107,33 +144,142 @@ impl pallet_timestamp::Trait for Test {
|
||||
type MinimumPeriod = MinimumPeriod;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u128 = 1;
|
||||
}
|
||||
|
||||
impl pallet_balances::Trait for Test {
|
||||
type Balance = u128;
|
||||
type DustRemoval = ();
|
||||
type Event = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
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 SlashDeferDuration: EraIndex = 0;
|
||||
pub const AttestationPeriod: u64 = 100;
|
||||
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
|
||||
pub const MaxNominatorRewardedPerValidator: u32 = 64;
|
||||
pub const ElectionLookahead: u64 = 0;
|
||||
pub const StakingUnsignedPriority: u64 = u64::max_value() / 2;
|
||||
}
|
||||
|
||||
pub struct CurrencyToVoteHandler;
|
||||
|
||||
impl Convert<u128, u128> for CurrencyToVoteHandler {
|
||||
fn convert(x: u128) -> u128 {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
impl Convert<u128, u64> for CurrencyToVoteHandler {
|
||||
fn convert(x: u128) -> u64 {
|
||||
x.saturated_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl pallet_staking::Trait for Test {
|
||||
type RewardRemainder = ();
|
||||
type CurrencyToVote = CurrencyToVoteHandler;
|
||||
type Event = ();
|
||||
type Currency = Balances;
|
||||
type Slash = ();
|
||||
type Reward = ();
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SlashDeferDuration = SlashDeferDuration;
|
||||
type SlashCancelOrigin = frame_system::EnsureRoot<Self::AccountId>;
|
||||
type SessionInterface = Self;
|
||||
type UnixTime = pallet_timestamp::Module<Test>;
|
||||
type RewardCurve = RewardCurve;
|
||||
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
|
||||
type NextNewSession = Session;
|
||||
type ElectionLookahead = ElectionLookahead;
|
||||
type Call = Call;
|
||||
type UnsignedPriority = StakingUnsignedPriority;
|
||||
type MaxIterations = ();
|
||||
type MinSolutionScoreBump = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub OffencesWeightSoftLimit: Weight = Perbill::from_percent(60) * MaximumBlockWeight::get();
|
||||
}
|
||||
|
||||
impl pallet_offences::Trait for Test {
|
||||
type Event = ();
|
||||
type IdentificationTuple = pallet_session::historical::IdentificationTuple<Self>;
|
||||
type OnOffenceHandler = Staking;
|
||||
type WeightSoftLimit = OffencesWeightSoftLimit;
|
||||
}
|
||||
|
||||
impl Trait for Test {
|
||||
type EpochDuration = EpochDuration;
|
||||
type ExpectedBlockTime = ExpectedBlockTime;
|
||||
type EpochChangeTrigger = crate::ExternalTrigger;
|
||||
|
||||
type KeyOwnerProofSystem = Historical;
|
||||
|
||||
type KeyOwnerProof =
|
||||
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, AuthorityId)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
AuthorityId,
|
||||
)>>::IdentificationTuple;
|
||||
|
||||
type HandleEquivocation = super::EquivocationHandler<Self::KeyOwnerIdentification, Offences>;
|
||||
}
|
||||
|
||||
pub fn new_test_ext(authorities_len: usize) -> (Vec<AuthorityPair>, sp_io::TestExternalities) {
|
||||
let pairs = (0..authorities_len).map(|i| {
|
||||
AuthorityPair::from_seed(&U256::from(i).into())
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
GenesisConfig {
|
||||
authorities: pairs.iter().map(|a| (a.public(), 1)).collect(),
|
||||
}.assimilate_storage::<Test>(&mut t).unwrap();
|
||||
(pairs, t.into())
|
||||
}
|
||||
pub type Balances = pallet_balances::Module<Test>;
|
||||
pub type Historical = pallet_session::historical::Module<Test>;
|
||||
pub type Offences = pallet_offences::Module<Test>;
|
||||
pub type Session = pallet_session::Module<Test>;
|
||||
pub type Staking = pallet_staking::Module<Test>;
|
||||
pub type System = frame_system::Module<Test>;
|
||||
pub type Timestamp = pallet_timestamp::Module<Test>;
|
||||
pub type Babe = Module<Test>;
|
||||
|
||||
pub fn go_to_block(n: u64, s: u64) {
|
||||
use frame_support::traits::OnFinalize;
|
||||
|
||||
System::on_finalize(System::block_number());
|
||||
Session::on_finalize(System::block_number());
|
||||
Staking::on_finalize(System::block_number());
|
||||
|
||||
let parent_hash = if System::block_number() > 1 {
|
||||
let hdr = System::finalize();
|
||||
hdr.hash()
|
||||
} else {
|
||||
System::parent_hash()
|
||||
};
|
||||
|
||||
let pre_digest = make_secondary_plain_pre_digest(0, s);
|
||||
System::initialize(&n, &Default::default(), &Default::default(), &pre_digest, InitKind::Full);
|
||||
|
||||
System::initialize(&n, &parent_hash, &Default::default(), &pre_digest, InitKind::Full);
|
||||
System::set_block_number(n);
|
||||
Timestamp::set_timestamp(n);
|
||||
|
||||
if s > 1 {
|
||||
CurrentSlot::put(s);
|
||||
}
|
||||
// includes a call into `Babe::do_initialize`.
|
||||
|
||||
System::on_initialize(n);
|
||||
Session::on_initialize(n);
|
||||
Staking::on_initialize(n);
|
||||
}
|
||||
|
||||
/// Slots will grow accordingly to blocks
|
||||
@@ -145,6 +291,19 @@ pub fn progress_to_block(n: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress to the first block at the given session
|
||||
pub fn start_session(session_index: SessionIndex) {
|
||||
let missing = (session_index - Session::current_index()) * 3;
|
||||
progress_to_block(System::block_number() + missing as u64 + 1);
|
||||
assert_eq!(Session::current_index(), session_index);
|
||||
}
|
||||
|
||||
/// Progress to the first block at the given era
|
||||
pub fn start_era(era_index: EraIndex) {
|
||||
start_session((era_index * 3).into());
|
||||
assert_eq!(Staking::current_era(), Some(era_index));
|
||||
}
|
||||
|
||||
pub fn make_pre_digest(
|
||||
authority_index: sp_consensus_babe::AuthorityIndex,
|
||||
slot_number: sp_consensus_babe::SlotNumber,
|
||||
@@ -177,6 +336,124 @@ pub fn make_secondary_plain_pre_digest(
|
||||
Digest { logs: vec![log] }
|
||||
}
|
||||
|
||||
pub type System = frame_system::Module<Test>;
|
||||
pub type Babe = Module<Test>;
|
||||
pub type Session = pallet_session::Module<Test>;
|
||||
pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities {
|
||||
new_test_ext_with_pairs(authorities_len).1
|
||||
}
|
||||
|
||||
pub fn new_test_ext_with_pairs(authorities_len: usize) -> (Vec<AuthorityPair>, sp_io::TestExternalities) {
|
||||
let pairs = (0..authorities_len).map(|i| {
|
||||
AuthorityPair::from_seed(&U256::from(i).into())
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let public = pairs.iter().map(|p| p.public()).collect();
|
||||
|
||||
(pairs, new_test_ext_raw_authorities(public))
|
||||
}
|
||||
|
||||
pub fn new_test_ext_raw_authorities(authorities: Vec<AuthorityId>) -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<Test>()
|
||||
.unwrap();
|
||||
|
||||
// stashes are the index.
|
||||
let session_keys: Vec<_> = authorities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, k)| {
|
||||
(
|
||||
i as u64,
|
||||
i as u64,
|
||||
MockSessionKeys {
|
||||
babe_authority: AuthorityId::from(k.clone()),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 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 balances: Vec<_> = (0..authorities.len())
|
||||
.map(|i| (i as u64, 10_000_000))
|
||||
.collect();
|
||||
|
||||
// NOTE: this will initialize the babe authorities
|
||||
// through OneSessionHandler::on_genesis_session
|
||||
pallet_session::GenesisConfig::<Test> { keys: session_keys }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
pallet_balances::GenesisConfig::<Test> { balances }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let staking_config = pallet_staking::GenesisConfig::<Test> {
|
||||
stakers,
|
||||
validator_count: 8,
|
||||
force_era: pallet_staking::Forcing::ForceNew,
|
||||
minimum_validator_count: 0,
|
||||
invulnerables: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
staking_config.assimilate_storage(&mut t).unwrap();
|
||||
|
||||
t.into()
|
||||
}
|
||||
|
||||
/// Creates an equivocation at the current block, by generating two headers.
|
||||
pub fn generate_equivocation_proof(
|
||||
offender_authority_index: u32,
|
||||
offender_authority_pair: &AuthorityPair,
|
||||
slot_number: SlotNumber,
|
||||
) -> sp_consensus_babe::EquivocationProof<Header> {
|
||||
use sp_consensus_babe::digests::CompatibleDigestItem;
|
||||
|
||||
let current_block = System::block_number();
|
||||
let current_slot = CurrentSlot::get();
|
||||
|
||||
let make_header = || {
|
||||
let parent_hash = System::parent_hash();
|
||||
let pre_digest = make_secondary_plain_pre_digest(offender_authority_index, slot_number);
|
||||
System::initialize(¤t_block, &parent_hash, &Default::default(), &pre_digest, InitKind::Full);
|
||||
System::set_block_number(current_block);
|
||||
Timestamp::set_timestamp(current_block);
|
||||
System::finalize()
|
||||
};
|
||||
|
||||
// sign the header prehash and sign it, adding it to the block as the seal
|
||||
// digest item
|
||||
let seal_header = |header: &mut Header| {
|
||||
let prehash = header.hash();
|
||||
let seal = <DigestItem as CompatibleDigestItem>::babe_seal(
|
||||
offender_authority_pair.sign(prehash.as_ref()),
|
||||
);
|
||||
header.digest_mut().push(seal);
|
||||
};
|
||||
|
||||
// generate two headers at the current block
|
||||
let mut h1 = make_header();
|
||||
let mut h2 = make_header();
|
||||
|
||||
seal_header(&mut h1);
|
||||
seal_header(&mut h2);
|
||||
|
||||
// restore previous runtime state
|
||||
go_to_block(current_block, current_slot);
|
||||
|
||||
sp_consensus_babe::EquivocationProof {
|
||||
slot_number,
|
||||
offender: offender_authority_pair.public(),
|
||||
first_header: h1,
|
||||
second_header: h2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
|
||||
//! Consensus extension module tests for BABE consensus.
|
||||
|
||||
use super::*;
|
||||
use super::{Call, *};
|
||||
use frame_support::{
|
||||
assert_err, assert_ok,
|
||||
traits::{Currency, OnFinalize},
|
||||
};
|
||||
use mock::*;
|
||||
use frame_support::traits::OnFinalize;
|
||||
use pallet_session::ShouldEndSession;
|
||||
use sp_core::crypto::IsWrappedBy;
|
||||
use sp_consensus_babe::AllowedSlots;
|
||||
use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof};
|
||||
use sp_core::crypto::{IsWrappedBy, Pair};
|
||||
|
||||
const EMPTY_RANDOMNESS: [u8; 32] = [
|
||||
74, 25, 49, 128, 53, 97, 244, 49,
|
||||
@@ -40,14 +43,14 @@ fn empty_randomness_is_correct() {
|
||||
|
||||
#[test]
|
||||
fn initial_values() {
|
||||
new_test_ext(4).1.execute_with(|| {
|
||||
new_test_ext(4).execute_with(|| {
|
||||
assert_eq!(Babe::authorities().len(), 4)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_module() {
|
||||
new_test_ext(4).1.execute_with(|| {
|
||||
new_test_ext(4).execute_with(|| {
|
||||
assert!(!Babe::should_end_session(0), "Genesis does not change sessions");
|
||||
assert!(!Babe::should_end_session(200000),
|
||||
"BABE does not include the block number in epoch calculations");
|
||||
@@ -56,7 +59,7 @@ fn check_module() {
|
||||
|
||||
#[test]
|
||||
fn first_block_epoch_zero_start() {
|
||||
let (pairs, mut ext) = new_test_ext(4);
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(4);
|
||||
|
||||
ext.execute_with(|| {
|
||||
let genesis_slot = 100;
|
||||
@@ -124,7 +127,7 @@ fn first_block_epoch_zero_start() {
|
||||
|
||||
#[test]
|
||||
fn authority_index() {
|
||||
new_test_ext(4).1.execute_with(|| {
|
||||
new_test_ext(4).execute_with(|| {
|
||||
assert_eq!(
|
||||
Babe::find_author((&[(BABE_ENGINE_ID, &[][..])]).into_iter().cloned()), None,
|
||||
"Trivially invalid authorities are ignored")
|
||||
@@ -133,7 +136,7 @@ fn authority_index() {
|
||||
|
||||
#[test]
|
||||
fn can_predict_next_epoch_change() {
|
||||
new_test_ext(0).1.execute_with(|| {
|
||||
new_test_ext(1).execute_with(|| {
|
||||
assert_eq!(<Test as Trait>::EpochDuration::get(), 3);
|
||||
// this sets the genesis slot to 6;
|
||||
go_to_block(1, 6);
|
||||
@@ -154,7 +157,7 @@ fn can_predict_next_epoch_change() {
|
||||
|
||||
#[test]
|
||||
fn can_enact_next_config() {
|
||||
new_test_ext(0).1.execute_with(|| {
|
||||
new_test_ext(1).execute_with(|| {
|
||||
assert_eq!(<Test as Trait>::EpochDuration::get(), 3);
|
||||
// this sets the genesis slot to 6;
|
||||
go_to_block(1, 6);
|
||||
@@ -183,3 +186,402 @@ fn can_enact_next_config() {
|
||||
assert_eq!(header.digest.logs[2], consensus_digest.clone())
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_current_session_works() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
let validators = Session::validators();
|
||||
|
||||
// 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(1, validator),
|
||||
pallet_staking::Exposure {
|
||||
total: 10_000,
|
||||
own: 10_000,
|
||||
others: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_validator_id = Session::validators()[offending_validator_index];
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// generate an equivocation proof. it creates two headers at the given
|
||||
// slot with different block hashes and signed by the given key
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// report the equivocation
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof)
|
||||
.unwrap();
|
||||
|
||||
// start a new era so that the results of the offence report
|
||||
// are applied at era end
|
||||
start_era(2);
|
||||
|
||||
// check that the balance of offending validator is slashed 100%.
|
||||
assert_eq!(
|
||||
Balances::total_balance(&offending_validator_id),
|
||||
10_000_000 - 10_000
|
||||
);
|
||||
assert_eq!(Staking::slashable_balance_of(&offending_validator_id), 0);
|
||||
assert_eq!(
|
||||
Staking::eras_stakers(2, offending_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 == offending_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_session_works() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_validator_id = Session::validators()[offending_validator_index];
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// generate an equivocation proof at the current slot
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// start a new era and report the equivocation
|
||||
// from the previous era
|
||||
start_era(2);
|
||||
|
||||
// check the balance of the offending validator
|
||||
assert_eq!(Balances::total_balance(&offending_validator_id), 10_000_000);
|
||||
assert_eq!(
|
||||
Staking::slashable_balance_of(&offending_validator_id),
|
||||
10_000
|
||||
);
|
||||
|
||||
// report the equivocation
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof)
|
||||
.unwrap();
|
||||
|
||||
// start a new era so that the results of the offence report
|
||||
// are applied at era end
|
||||
start_era(3);
|
||||
|
||||
// check that the balance of offending validator is slashed 100%.
|
||||
assert_eq!(
|
||||
Balances::total_balance(&offending_validator_id),
|
||||
10_000_000 - 10_000
|
||||
);
|
||||
assert_eq!(Staking::slashable_balance_of(&offending_validator_id), 0);
|
||||
assert_eq!(
|
||||
Staking::eras_stakers(3, offending_validator_id),
|
||||
pallet_staking::Exposure {
|
||||
total: 0,
|
||||
own: 0,
|
||||
others: vec![],
|
||||
},
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_invalid_key_owner_proof() {
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// generate an equivocation proof at the current slot
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let mut key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// we change the session index in the key ownership proof
|
||||
// which should make it invalid
|
||||
key_owner_proof.session = 0;
|
||||
assert_err!(
|
||||
Babe::report_equivocation_unsigned(
|
||||
Origin::none(),
|
||||
equivocation_proof.clone(),
|
||||
key_owner_proof
|
||||
),
|
||||
Error::<Test>::InvalidKeyOwnershipProof,
|
||||
);
|
||||
|
||||
// it should fail as well if we create a key owner proof
|
||||
// for a different authority than the offender
|
||||
let key = (sp_consensus_babe::KEY_TYPE, &authorities[1].0);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
// we need to progress to a new era to make sure that the key
|
||||
// ownership proof is properly checked, otherwise since the state
|
||||
// is still available the historical module will just check
|
||||
// against current session data.
|
||||
start_era(2);
|
||||
|
||||
assert_err!(
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof),
|
||||
Error::<Test>::InvalidKeyOwnershipProof,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_invalid_equivocation_proof() {
|
||||
use sp_runtime::traits::Header;
|
||||
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// we will use the validator at index 0 as the offending authority
|
||||
let offending_validator_index = 0;
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
// create the key ownership proof
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
let assert_invalid_equivocation = |equivocation_proof| {
|
||||
assert_err!(
|
||||
Babe::report_equivocation_unsigned(
|
||||
Origin::none(),
|
||||
equivocation_proof,
|
||||
key_owner_proof.clone(),
|
||||
),
|
||||
Error::<Test>::InvalidEquivocationProof,
|
||||
)
|
||||
};
|
||||
|
||||
// both headers have the same hash, no equivocation.
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.second_header = equivocation_proof.first_header.clone();
|
||||
assert_invalid_equivocation(equivocation_proof);
|
||||
|
||||
// missing preruntime digest from one header
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.first_header.digest_mut().logs.remove(0);
|
||||
assert_invalid_equivocation(equivocation_proof);
|
||||
|
||||
// missing seal from one header
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.first_header.digest_mut().logs.remove(1);
|
||||
assert_invalid_equivocation(equivocation_proof);
|
||||
|
||||
// invalid slot number in proof compared to runtime digest
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
equivocation_proof.slot_number = 0;
|
||||
assert_invalid_equivocation(equivocation_proof.clone());
|
||||
|
||||
// different slot numbers in headers
|
||||
let h1 = equivocation_proof.first_header;
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get() + 1,
|
||||
);
|
||||
|
||||
// use the header from the previous equivocation generated
|
||||
// at the previous slot
|
||||
equivocation_proof.first_header = h1.clone();
|
||||
|
||||
assert_invalid_equivocation(equivocation_proof.clone());
|
||||
|
||||
// invalid seal signature
|
||||
let mut equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get() + 1,
|
||||
);
|
||||
|
||||
// replace the seal digest with the digest from the
|
||||
// previous header at the previous slot
|
||||
equivocation_proof.first_header.digest_mut().pop();
|
||||
equivocation_proof
|
||||
.first_header
|
||||
.digest_mut()
|
||||
.push(h1.digest().logs().last().unwrap().clone());
|
||||
|
||||
assert_invalid_equivocation(equivocation_proof.clone());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_equivocation_validate_unsigned_prevents_duplicates() {
|
||||
use sp_runtime::transaction_validity::{
|
||||
InvalidTransaction, TransactionLongevity, TransactionPriority, TransactionSource,
|
||||
TransactionValidity, ValidTransaction,
|
||||
};
|
||||
|
||||
let (pairs, mut ext) = new_test_ext_with_pairs(3);
|
||||
|
||||
ext.execute_with(|| {
|
||||
start_era(1);
|
||||
|
||||
let authorities = Babe::authorities();
|
||||
|
||||
// generate and report an equivocation for the validator at index 0
|
||||
let offending_validator_index = 0;
|
||||
let offending_authority_pair = pairs
|
||||
.into_iter()
|
||||
.find(|p| p.public() == authorities[offending_validator_index].0)
|
||||
.unwrap();
|
||||
|
||||
let equivocation_proof = generate_equivocation_proof(
|
||||
offending_validator_index as u32,
|
||||
&offending_authority_pair,
|
||||
CurrentSlot::get(),
|
||||
);
|
||||
|
||||
let key = (
|
||||
sp_consensus_babe::KEY_TYPE,
|
||||
&offending_authority_pair.public(),
|
||||
);
|
||||
let key_owner_proof = Historical::prove(key).unwrap();
|
||||
|
||||
let inner =
|
||||
Call::report_equivocation_unsigned(equivocation_proof.clone(), key_owner_proof.clone());
|
||||
|
||||
// only local/inblock reports are allowed
|
||||
assert_eq!(
|
||||
<Babe as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
|
||||
TransactionSource::External,
|
||||
&inner,
|
||||
),
|
||||
InvalidTransaction::Call.into(),
|
||||
);
|
||||
|
||||
// the transaction is valid when passed as local
|
||||
let tx_tag = (offending_authority_pair.public(), CurrentSlot::get());
|
||||
assert_eq!(
|
||||
<Babe as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
|
||||
TransactionSource::Local,
|
||||
&inner,
|
||||
),
|
||||
TransactionValidity::Ok(ValidTransaction {
|
||||
priority: TransactionPriority::max_value(),
|
||||
requires: vec![],
|
||||
provides: vec![("BabeEquivocation", tx_tag).encode()],
|
||||
longevity: TransactionLongevity::max_value(),
|
||||
propagate: false,
|
||||
})
|
||||
);
|
||||
|
||||
// the pre dispatch checks should also pass
|
||||
assert_ok!(<Babe as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&inner));
|
||||
|
||||
// we submit the report
|
||||
Babe::report_equivocation_unsigned(Origin::none(), equivocation_proof, key_owner_proof)
|
||||
.unwrap();
|
||||
|
||||
// the report should now be considered stale and the transaction is invalid
|
||||
assert_err!(
|
||||
<Babe as sp_runtime::traits::ValidateUnsigned>::pre_dispatch(&inner),
|
||||
InvalidTransaction::Stale,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user