#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use bp_beefy::{ChainWithBeefy, InitializationData};
use sp_std::{boxed::Box, prelude::*};
pub use pallet::*;
mod utils;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod mock_chain;
pub const LOG_TARGET: &str = "runtime::bridge-beefy";
pub type BridgedChain<T, I> = <T as Config<I>>::BridgedChain;
pub type BridgedBlockNumber<T, I> = bp_runtime::BlockNumberOf<BridgedChain<T, I>>;
pub type BridgedBlockHash<T, I> = bp_runtime::HashOf<BridgedChain<T, I>>;
pub type InitializationDataOf<T, I> =
InitializationData<BridgedBlockNumber<T, I>, bp_beefy::MmrHashOf<BridgedChain<T, I>>>;
pub type BridgedBeefyCommitmentHasher<T, I> = bp_beefy::BeefyCommitmentHasher<BridgedChain<T, I>>;
pub type BridgedBeefyAuthorityId<T, I> = bp_beefy::BeefyAuthorityIdOf<BridgedChain<T, I>>;
pub type BridgedBeefyAuthoritySet<T, I> = bp_beefy::BeefyAuthoritySetOf<BridgedChain<T, I>>;
pub type BridgedBeefyAuthoritySetInfo<T, I> = bp_beefy::BeefyAuthoritySetInfoOf<BridgedChain<T, I>>;
pub type BridgedBeefySignedCommitment<T, I> = bp_beefy::BeefySignedCommitmentOf<BridgedChain<T, I>>;
pub type BridgedMmrHashing<T, I> = bp_beefy::MmrHashingOf<BridgedChain<T, I>>;
pub type BridgedMmrHash<T, I> = bp_beefy::MmrHashOf<BridgedChain<T, I>>;
pub type BridgedBeefyMmrLeafExtra<T, I> = bp_beefy::BeefyMmrLeafExtraOf<BridgedChain<T, I>>;
pub type BridgedMmrProof<T, I> = bp_beefy::MmrProofOf<BridgedChain<T, I>>;
pub type BridgedBeefyMmrLeaf<T, I> = bp_beefy::BeefyMmrLeafOf<BridgedChain<T, I>>;
pub type ImportedCommitment<T, I> = bp_beefy::ImportedCommitment<
BridgedBlockNumber<T, I>,
BridgedBlockHash<T, I>,
BridgedMmrHash<T, I>,
>;
#[derive(codec::Encode, codec::Decode, scale_info::TypeInfo)]
pub struct ImportedCommitmentsInfoData<BlockNumber> {
best_block_number: BlockNumber,
next_block_number_index: u32,
}
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use super::*;
use bp_runtime::{BasicOperatingMode, OwnedBridgeModule};
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
#[pallet::constant]
type MaxRequests: Get<u32>;
#[pallet::constant]
type CommitmentsToKeep: Get<u32>;
type BridgedChain: ChainWithBeefy;
}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
fn on_initialize(_n: BlockNumberFor<T>) -> frame_support::weights::Weight {
<RequestCount<T, I>>::mutate(|count| *count = count.saturating_sub(1));
Weight::from_parts(0, 0)
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
}
}
impl<T: Config<I>, I: 'static> OwnedBridgeModule<T> for Pallet<T, I> {
const LOG_TARGET: &'static str = LOG_TARGET;
type OwnerStorage = PalletOwner<T, I>;
type OperatingMode = BasicOperatingMode;
type OperatingModeStorage = PalletOperatingMode<T, I>;
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I>
where
BridgedMmrHashing<T, I>: 'static + Send + Sync,
{
#[pallet::call_index(0)]
#[pallet::weight((T::DbWeight::get().reads_writes(2, 3), DispatchClass::Operational))]
pub fn initialize(
origin: OriginFor<T>,
init_data: InitializationDataOf<T, I>,
) -> DispatchResult {
Self::ensure_owner_or_root(origin)?;
let is_initialized = <ImportedCommitmentsInfo<T, I>>::exists();
ensure!(!is_initialized, <Error<T, I>>::AlreadyInitialized);
log::info!(target: LOG_TARGET, "Initializing bridge BEEFY pallet: {:?}", init_data);
Ok(initialize::<T, I>(init_data)?)
}
#[pallet::call_index(1)]
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_owner(origin: OriginFor<T>, new_owner: Option<T::AccountId>) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::set_owner(origin, new_owner)
}
#[pallet::call_index(2)]
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_operating_mode(
origin: OriginFor<T>,
operating_mode: BasicOperatingMode,
) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::set_operating_mode(origin, operating_mode)
}
#[pallet::call_index(3)]
#[pallet::weight(0)]
pub fn submit_commitment(
origin: OriginFor<T>,
commitment: BridgedBeefySignedCommitment<T, I>,
validator_set: BridgedBeefyAuthoritySet<T, I>,
mmr_leaf: Box<BridgedBeefyMmrLeaf<T, I>>,
mmr_proof: BridgedMmrProof<T, I>,
) -> DispatchResult
where
BridgedBeefySignedCommitment<T, I>: Clone,
{
Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
ensure_signed(origin)?;
ensure!(Self::request_count() < T::MaxRequests::get(), <Error<T, I>>::TooManyRequests);
let commitments_info =
ImportedCommitmentsInfo::<T, I>::get().ok_or(Error::<T, I>::NotInitialized)?;
ensure!(
commitment.commitment.block_number > commitments_info.best_block_number,
Error::<T, I>::OldCommitment
);
let current_authority_set_info = CurrentAuthoritySetInfo::<T, I>::get();
let mmr_root = utils::verify_commitment::<T, I>(
&commitment,
¤t_authority_set_info,
&validator_set,
)?;
utils::verify_beefy_mmr_leaf::<T, I>(&mmr_leaf, mmr_proof, mmr_root)?;
RequestCount::<T, I>::mutate(|count| *count += 1);
if mmr_leaf.beefy_next_authority_set.id > current_authority_set_info.id {
CurrentAuthoritySetInfo::<T, I>::put(mmr_leaf.beefy_next_authority_set);
}
let block_number_index = commitments_info.next_block_number_index;
let to_prune = ImportedBlockNumbers::<T, I>::try_get(block_number_index);
ImportedCommitments::<T, I>::insert(
commitment.commitment.block_number,
ImportedCommitment::<T, I> {
parent_number_and_hash: mmr_leaf.parent_number_and_hash,
mmr_root,
},
);
ImportedBlockNumbers::<T, I>::insert(
block_number_index,
commitment.commitment.block_number,
);
ImportedCommitmentsInfo::<T, I>::put(ImportedCommitmentsInfoData {
best_block_number: commitment.commitment.block_number,
next_block_number_index: (block_number_index + 1) % T::CommitmentsToKeep::get(),
});
if let Ok(old_block_number) = to_prune {
log::debug!(
target: LOG_TARGET,
"Pruning commitment for old block: {:?}.",
old_block_number
);
ImportedCommitments::<T, I>::remove(old_block_number);
}
log::info!(
target: LOG_TARGET,
"Successfully imported commitment for block {:?}",
commitment.commitment.block_number,
);
Ok(())
}
}
#[pallet::storage]
#[pallet::getter(fn request_count)]
pub type RequestCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
pub type ImportedCommitmentsInfo<T: Config<I>, I: 'static = ()> =
StorageValue<_, ImportedCommitmentsInfoData<BridgedBlockNumber<T, I>>>;
#[pallet::storage]
pub(super) type ImportedBlockNumbers<T: Config<I>, I: 'static = ()> =
StorageMap<_, Identity, u32, BridgedBlockNumber<T, I>>;
#[pallet::storage]
pub type ImportedCommitments<T: Config<I>, I: 'static = ()> =
StorageMap<_, Blake2_128Concat, BridgedBlockNumber<T, I>, ImportedCommitment<T, I>>;
#[pallet::storage]
pub type CurrentAuthoritySetInfo<T: Config<I>, I: 'static = ()> =
StorageValue<_, BridgedBeefyAuthoritySetInfo<T, I>, ValueQuery>;
#[pallet::storage]
pub type PalletOwner<T: Config<I>, I: 'static = ()> =
StorageValue<_, T::AccountId, OptionQuery>;
#[pallet::storage]
pub type PalletOperatingMode<T: Config<I>, I: 'static = ()> =
StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub owner: Option<T::AccountId>,
pub init_data: Option<InitializationDataOf<T, I>>,
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
if let Some(ref owner) = self.owner {
<PalletOwner<T, I>>::put(owner);
}
if let Some(init_data) = self.init_data.clone() {
initialize::<T, I>(init_data)
.expect("invalid initialization data of BEEFY bridge pallet");
} else {
<PalletOperatingMode<T, I>>::put(BasicOperatingMode::Halted);
}
}
}
#[pallet::error]
pub enum Error<T, I = ()> {
NotInitialized,
AlreadyInitialized,
InvalidInitialAuthoritySet,
TooManyRequests,
OldCommitment,
InvalidCommitmentValidatorSetId,
InvalidValidatorSetId,
InvalidCommitmentSignaturesLen,
InvalidValidatorSetLen,
NotEnoughCorrectSignatures,
MmrRootMissingFromCommitment,
MmrProofVerificationFailed,
InvalidValidatorSetRoot,
BridgeModule(bp_runtime::OwnedBridgeModuleError),
}
pub(super) fn initialize<T: Config<I>, I: 'static>(
init_data: InitializationDataOf<T, I>,
) -> Result<(), Error<T, I>> {
if init_data.authority_set.len == 0 {
return Err(Error::<T, I>::InvalidInitialAuthoritySet)
}
CurrentAuthoritySetInfo::<T, I>::put(init_data.authority_set);
<PalletOperatingMode<T, I>>::put(init_data.operating_mode);
ImportedCommitmentsInfo::<T, I>::put(ImportedCommitmentsInfoData {
best_block_number: init_data.best_block_number,
next_block_number_index: 0,
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use bp_runtime::{BasicOperatingMode, OwnedBridgeModuleError};
use bp_test_utils::generate_owned_bridge_module_tests;
use frame_support::{assert_noop, assert_ok, traits::Get};
use mock::*;
use mock_chain::*;
use sp_consensus_beefy::mmr::BeefyAuthoritySet;
use sp_runtime::DispatchError;
fn next_block() {
use frame_support::traits::OnInitialize;
let current_number = frame_system::Pallet::<TestRuntime>::block_number();
frame_system::Pallet::<TestRuntime>::set_block_number(current_number + 1);
let _ = Pallet::<TestRuntime>::on_initialize(current_number);
}
fn import_header_chain(headers: Vec<HeaderAndCommitment>) {
for header in headers {
if header.commitment.is_some() {
assert_ok!(import_commitment(header));
}
}
}
#[test]
fn fails_to_initialize_if_already_initialized() {
run_test_with_initialize(32, || {
assert_noop!(
Pallet::<TestRuntime>::initialize(
RuntimeOrigin::root(),
InitializationData {
operating_mode: BasicOperatingMode::Normal,
best_block_number: 0,
authority_set: BeefyAuthoritySet {
id: 0,
len: 1,
keyset_commitment: [0u8; 32].into()
}
}
),
Error::<TestRuntime, ()>::AlreadyInitialized,
);
});
}
#[test]
fn fails_to_initialize_if_authority_set_is_empty() {
run_test(|| {
assert_noop!(
Pallet::<TestRuntime>::initialize(
RuntimeOrigin::root(),
InitializationData {
operating_mode: BasicOperatingMode::Normal,
best_block_number: 0,
authority_set: BeefyAuthoritySet {
id: 0,
len: 0,
keyset_commitment: [0u8; 32].into()
}
}
),
Error::<TestRuntime, ()>::InvalidInitialAuthoritySet,
);
});
}
#[test]
fn fails_to_import_commitment_if_halted() {
run_test_with_initialize(1, || {
assert_ok!(Pallet::<TestRuntime>::set_operating_mode(
RuntimeOrigin::root(),
BasicOperatingMode::Halted
));
assert_noop!(
import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()),
Error::<TestRuntime, ()>::BridgeModule(OwnedBridgeModuleError::Halted),
);
})
}
#[test]
fn fails_to_import_commitment_if_too_many_requests() {
run_test_with_initialize(1, || {
let max_requests = <<TestRuntime as Config>::MaxRequests as Get<u32>>::get() as u64;
let mut chain = ChainBuilder::new(1);
for _ in 0..max_requests + 2 {
chain = chain.append_finalized_header();
}
for i in 0..max_requests {
assert_ok!(import_commitment(chain.header(i + 1)));
}
assert_noop!(
import_commitment(chain.header(max_requests + 1)),
Error::<TestRuntime, ()>::TooManyRequests,
);
next_block();
assert_ok!(import_commitment(chain.header(max_requests + 1)));
assert_noop!(
import_commitment(chain.header(max_requests + 2)),
Error::<TestRuntime, ()>::TooManyRequests,
);
})
}
#[test]
fn fails_to_import_commitment_if_not_initialized() {
run_test(|| {
assert_noop!(
import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()),
Error::<TestRuntime, ()>::NotInitialized,
);
})
}
#[test]
fn submit_commitment_works_with_long_chain_with_handoffs() {
run_test_with_initialize(3, || {
let chain = ChainBuilder::new(3)
.append_finalized_header()
.append_default_headers(16) .append_finalized_header() .append_default_headers(16) .append_handoff_header(9) .append_default_headers(8) .append_finalized_header() .append_default_headers(8) .append_handoff_header(17) .append_default_headers(4) .append_finalized_header() .append_default_headers(4); import_header_chain(chain.to_chain());
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number,
58
);
assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().id, 2);
assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().len, 17);
let imported_commitment = ImportedCommitments::<TestRuntime>::get(58).unwrap();
assert_eq!(
imported_commitment,
bp_beefy::ImportedCommitment {
parent_number_and_hash: (57, chain.header(57).header.hash()),
mmr_root: chain.header(58).mmr_root,
},
);
})
}
#[test]
fn commitment_pruning_works() {
run_test_with_initialize(3, || {
let commitments_to_keep = <TestRuntime as Config<()>>::CommitmentsToKeep::get();
let commitments_to_import: Vec<HeaderAndCommitment> = ChainBuilder::new(3)
.append_finalized_headers(commitments_to_keep as usize + 2)
.to_chain();
for index in 0..commitments_to_keep {
next_block();
import_commitment(commitments_to_import[index as usize].clone())
.expect("must succeed");
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
(index + 1) % commitments_to_keep
);
}
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number,
commitments_to_keep as TestBridgedBlockNumber
);
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
0
);
for index in 0..commitments_to_keep {
assert!(ImportedCommitments::<TestRuntime>::get(
index as TestBridgedBlockNumber + 1
)
.is_some());
assert_eq!(
ImportedBlockNumbers::<TestRuntime>::get(index),
Some(Into::into(index + 1)),
);
}
next_block();
import_commitment(commitments_to_import[commitments_to_keep as usize].clone())
.expect("must succeed");
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
1
);
assert!(ImportedCommitments::<TestRuntime>::get(
commitments_to_keep as TestBridgedBlockNumber + 1
)
.is_some());
assert_eq!(
ImportedBlockNumbers::<TestRuntime>::get(0),
Some(Into::into(commitments_to_keep + 1)),
);
assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
next_block();
import_commitment(commitments_to_import[commitments_to_keep as usize + 1].clone())
.expect("must succeed");
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
2
);
assert!(ImportedCommitments::<TestRuntime>::get(
commitments_to_keep as TestBridgedBlockNumber + 2
)
.is_some());
assert_eq!(
ImportedBlockNumbers::<TestRuntime>::get(1),
Some(Into::into(commitments_to_keep + 2)),
);
assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
assert!(ImportedCommitments::<TestRuntime>::get(2).is_none());
});
}
generate_owned_bridge_module_tests!(BasicOperatingMode::Normal, BasicOperatingMode::Halted);
}