mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 23:57:56 +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:
@@ -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