Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

BEEFY: add digest log on consensus reset #14765

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frame/beefy/src/default_weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ impl crate::WeightInfo for () {
// fetching set id -> session index mappings
.saturating_add(DbWeight::get().reads(2))
}

fn set_new_genesis() -> Weight {
DbWeight::get().writes(1)
}
}
51 changes: 48 additions & 3 deletions frame/beefy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const LOG_TARGET: &str = "runtime::beefy";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_system::pallet_prelude::BlockNumberFor;
use frame_system::{ensure_root, pallet_prelude::BlockNumberFor};

#[pallet::config]
pub trait Config: frame_system::Config {
Expand Down Expand Up @@ -152,8 +152,8 @@ pub mod pallet {
StorageMap<_, Twox64Concat, sp_consensus_beefy::ValidatorSetId, SessionIndex>;

/// Block number where BEEFY consensus is enabled/started.
/// By changing this (through governance or sudo), BEEFY consensus is effectively
/// restarted from the new block number.
/// By changing this (through privileged `set_new_genesis()`), BEEFY consensus is effectively
/// restarted from the newly set block number.
#[pallet::storage]
#[pallet::getter(fn genesis_block)]
pub(super) type GenesisBlock<T: Config> =
Expand Down Expand Up @@ -198,6 +198,8 @@ pub mod pallet {
InvalidEquivocationProof,
/// A given equivocation report is valid but already previously reported.
DuplicateOffenceReport,
/// Submitted configuration is invalid.
InvalidConfiguration,
}

#[pallet::call]
Expand Down Expand Up @@ -265,6 +267,24 @@ pub mod pallet {
)?;
Ok(Pays::No.into())
}

/// Reset BEEFY consensus by setting a new BEEFY genesis block.
///
/// Note: new genesis cannot be set to a past block.
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::set_new_genesis())]
pub fn set_new_genesis(
origin: OriginFor<T>,
genesis_block: BlockNumberFor<T>,
) -> DispatchResult {
ensure_root(origin)?;
ensure!(
genesis_block >= frame_system::Pallet::<T>::block_number(),
Error::<T>::InvalidConfiguration
);
GenesisBlock::<T>::put(Some(genesis_block));
Ok(())
}
}

#[pallet::validate_unsigned]
Expand All @@ -279,6 +299,30 @@ pub mod pallet {
Self::validate_unsigned(source, call)
}
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
// weight used by `on_finalize()` hook
frame_support::weights::constants::RocksDbWeight::get().reads(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this isn't technically correct, because at BEEFY genesis block we'll be doing 3 db reads (Authorities and ValidatorSetId in addition to GenesisBlock). So probably it should be frame_support::weights::constants::RocksDbWeight::get().reads(if GenesisBlock::<T>::get() == Some(n) { 3 } else { 1 }). Don't think it is critical here, though :)

}

fn on_finalize(n: BlockNumberFor<T>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly this is needed because genesis_block can be a future block. But do we need to allow genesis_block to be a future block ? Could we just reset from the current block ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting from current block could also work and would simplify this code - no hook required anymore, just emit digest log when the reset call happens, BUT in practice we'd lose control of selecting the actual BEEFY genesis block number..

Imagine we need to start/reset BEEFY on Kusama or Polkadot: we need to go through governance and propose a genesis block number. It cannot be in the past because past blocks are immutable and it's practically impossible to time the governance enactment block number with the desired beefy genesis block number.

So in order to control beefy-genesis block M, what we do is propose in governance a beefy-genesis future block M > governance motion/proposal lifetime (estimated at block N) so that:

  • if motion fails, then it's noop
  • if it succeeds, the call to set beefy-genesis block will be mined at some block N' <= N < M - so it is valid and works

If we were to use current block then BEEFY genesis is simply the block when governance motion is enacted.

Regarding the need to control the number, I was talking to @andresilva that we'd likely want BEEFY genesis to be a session boundary block - although it's not really mandatory.

@bkchr wdyt? maybe we can use some other magic mechanism to delay a call to be executed on a predefined future block, and not have to rely on pallet-beefy to keep this logic?

// this check is only true when consensus is reset, so we can safely
// estimate the average weight is just one Db read.
if GenesisBlock::<T>::get() == Some(n) {
let validators = Authorities::<T>::get();
let set_id = ValidatorSetId::<T>::get();
if let Some(validator_set) = ValidatorSet::<T::BeefyId>::new(validators, set_id) {
let log = DigestItem::Consensus(
BEEFY_ENGINE_ID,
ConsensusLog::ConsensusReset(validator_set).encode(),
);
<frame_system::Pallet<T>>::deposit_log(log);
}
}
}
}
}

impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -452,4 +496,5 @@ impl<T: Config> IsMember<T::BeefyId> for Pallet<T> {

pub trait WeightInfo {
fn report_equivocation(validator_count: u32, max_nominators_per_validator: u32) -> Weight;
fn set_new_genesis() -> Weight;
}
47 changes: 25 additions & 22 deletions frame/beefy/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,31 +300,34 @@ pub fn new_test_ext_raw_authorities(authorities: Vec<BeefyId>) -> TestExternalit
t.into()
}

pub fn push_block(i: u32) {
System::on_finalize(System::block_number());
Session::on_finalize(System::block_number());
Staking::on_finalize(System::block_number());
Beefy::on_finalize(System::block_number());

let parent_hash = if System::block_number() > 1 {
let hdr = System::finalize();
hdr.hash()
} else {
System::parent_hash()
};

System::reset_events();
System::initialize(&(i as u64 + 1), &parent_hash, &Default::default());
System::set_block_number((i + 1).into());
Timestamp::set_timestamp(System::block_number() * 6000);

System::on_initialize(System::block_number());
Session::on_initialize(System::block_number());
Staking::on_initialize(System::block_number());
Beefy::on_initialize(System::block_number());
}

pub fn start_session(session_index: SessionIndex) {
for i in Session::current_index()..session_index {
System::on_finalize(System::block_number());
Session::on_finalize(System::block_number());
Staking::on_finalize(System::block_number());
Beefy::on_finalize(System::block_number());

let parent_hash = if System::block_number() > 1 {
let hdr = System::finalize();
hdr.hash()
} else {
System::parent_hash()
};

System::reset_events();
System::initialize(&(i as u64 + 1), &parent_hash, &Default::default());
System::set_block_number((i + 1).into());
Timestamp::set_timestamp(System::block_number() * 6000);

System::on_initialize(System::block_number());
Session::on_initialize(System::block_number());
Staking::on_initialize(System::block_number());
Beefy::on_initialize(System::block_number());
push_block(i);
}

assert_eq!(Session::current_index(), session_index);
}

Expand Down
78 changes: 76 additions & 2 deletions frame/beefy/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

use std::vec;

use codec::Encode;
use codec::{Decode, Encode};
use sp_consensus_beefy::{
check_equivocation_proof, generate_equivocation_proof, known_payloads::MMR_ROOT_ID,
Keyring as BeefyKeyring, Payload, ValidatorSet, KEY_TYPE as BEEFY_KEY_TYPE,
Expand All @@ -28,7 +28,7 @@ use sp_runtime::DigestItem;
use frame_support::{
assert_err, assert_ok,
dispatch::{GetDispatchInfo, Pays},
traits::{Currency, KeyOwnerProofSystem, OnInitialize},
traits::{Currency, KeyOwnerProofSystem, OnFinalize, OnInitialize},
};

use crate::{mock::*, Call, Config, Error, Weight, WeightInfo};
Expand Down Expand Up @@ -791,3 +791,77 @@ fn valid_equivocation_reports_dont_pay_fees() {
assert_eq!(post_info.pays_fee, Pays::Yes);
})
}

#[test]
fn set_new_genesis_works() {
let authorities = test_authorities();

new_test_ext_raw_authorities(authorities).execute_with(|| {
start_era(1);

let new_genesis = 10u64;
// the call for setting new genesis should work
assert_ok!(Beefy::set_new_genesis(RuntimeOrigin::root(), new_genesis));
// verify new genesis was set
assert_eq!(Beefy::genesis_block(), Some(new_genesis));

// setting to old block should fail
assert_err!(
Beefy::set_new_genesis(RuntimeOrigin::root(), 1u64),
Error::<Test>::InvalidConfiguration,
);
});
}

#[test]
fn deposit_consensus_reset_log_works() {
let authorities = test_authorities();

new_test_ext_raw_authorities(authorities).execute_with(|| {
fn find_consensus_reset_log() -> Option<ConsensusLog<BeefyId>> {
for log in System::digest().logs.iter_mut() {
if let DigestItem::Consensus(BEEFY_ENGINE_ID, inner_log) = log {
let inner_decoded =
ConsensusLog::<BeefyId>::decode(&mut &inner_log[..]).unwrap();
if matches!(inner_decoded, ConsensusLog::ConsensusReset(_)) {
return Some(inner_decoded)
}
}
}
None
}

start_era(1);
push_block(4);

// no consensus reset log
assert!(find_consensus_reset_log().is_none());

let new_genesis = 6u64;
// the call for setting new genesis should work
assert_ok!(Beefy::set_new_genesis(RuntimeOrigin::root(), new_genesis));
// verify new genesis was set
assert_eq!(Beefy::genesis_block(), Some(new_genesis));

// there should still be no digest (yet)
assert!(find_consensus_reset_log().is_none());

push_block(5);

// there should still be no digest (yet)
assert!(find_consensus_reset_log().is_none());

assert_eq!(System::block_number(), new_genesis);
Beefy::on_finalize(System::block_number());

// consensus reset log should be there now
let log = find_consensus_reset_log().unwrap();

// validate logged authorities
let validators = Beefy::authorities();
let set_id = Beefy::validator_set_id();
let want_validator_set = ValidatorSet::<BeefyId>::new(validators, set_id).unwrap();
let want = ConsensusLog::ConsensusReset(want_validator_set);
assert_eq!(want.encode(), log.encode());
});
}
3 changes: 3 additions & 0 deletions primitives/consensus/beefy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ pub enum ConsensusLog<AuthorityId: Codec> {
/// MMR root hash.
#[codec(index = 3)]
MmrRoot(MmrRootHash),
/// BEEFY consensus has been reset.
#[codec(index = 4)]
ConsensusReset(ValidatorSet<AuthorityId>),
}

/// BEEFY vote message.
Expand Down