// Copyright 2021 Parity Technologies (UK) Ltd. // This file is part of Parity Bridges Common. // Parity Bridges Common is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity Bridges Common is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . //! BEEFY bridge pallet. //! //! This pallet is an on-chain BEEFY light client for Substrate-based chains that are using the //! following pallets bundle: `pallet-mmr`, `pallet-beefy` and `pallet-beefy-mmr`. //! //! The pallet is able to verify MMR leaf proofs and BEEFY commitments, so it has access //! to the following data of the bridged chain: //! //! - header hashes //! - changes of BEEFY authorities //! - extra data of MMR leafs //! //! Given the header hash, other pallets are able to verify header-based proofs //! (e.g. storage proofs, transaction inclusion proofs, etc.). #![warn(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] use bp_beefy::{ChainWithBeefy, InitializationData}; use sp_std::{boxed::Box, prelude::*}; // Re-export in crate namespace for `construct_runtime!` pub use pallet::*; mod utils; #[cfg(test)] mod mock; #[cfg(test)] mod mock_chain; /// The target that will be used when publishing logs related to this pallet. pub const LOG_TARGET: &str = "runtime::bridge-beefy"; /// Configured bridged chain. pub type BridgedChain = >::BridgedChain; /// Block number, used by configured bridged chain. pub type BridgedBlockNumber = bp_runtime::BlockNumberOf>; /// Block hash, used by configured bridged chain. pub type BridgedBlockHash = bp_runtime::HashOf>; /// Pallet initialization data. pub type InitializationDataOf = InitializationData, bp_beefy::MmrHashOf>>; /// BEEFY commitment hasher, used by configured bridged chain. pub type BridgedBeefyCommitmentHasher = bp_beefy::BeefyCommitmentHasher>; /// BEEFY validator id, used by configured bridged chain. pub type BridgedBeefyAuthorityId = bp_beefy::BeefyAuthorityIdOf>; /// BEEFY validator set, used by configured bridged chain. pub type BridgedBeefyAuthoritySet = bp_beefy::BeefyAuthoritySetOf>; /// BEEFY authority set, used by configured bridged chain. pub type BridgedBeefyAuthoritySetInfo = bp_beefy::BeefyAuthoritySetInfoOf>; /// BEEFY signed commitment, used by configured bridged chain. pub type BridgedBeefySignedCommitment = bp_beefy::BeefySignedCommitmentOf>; /// MMR hashing algorithm, used by configured bridged chain. pub type BridgedMmrHashing = bp_beefy::MmrHashingOf>; /// MMR hashing output type of `BridgedMmrHashing`. pub type BridgedMmrHash = bp_beefy::MmrHashOf>; /// The type of the MMR leaf extra data used by the configured bridged chain. pub type BridgedBeefyMmrLeafExtra = bp_beefy::BeefyMmrLeafExtraOf>; /// BEEFY MMR proof type used by the pallet pub type BridgedMmrProof = bp_beefy::MmrProofOf>; /// MMR leaf type, used by configured bridged chain. pub type BridgedBeefyMmrLeaf = bp_beefy::BeefyMmrLeafOf>; /// Imported commitment data, stored by the pallet. pub type ImportedCommitment = bp_beefy::ImportedCommitment< BridgedBlockNumber, BridgedBlockHash, BridgedMmrHash, >; /// Some high level info about the imported commitments. #[derive(codec::Encode, codec::Decode, scale_info::TypeInfo)] pub struct ImportedCommitmentsInfoData { /// Best known block number, provided in a BEEFY commitment. However this is not /// the best proven block. The best proven block is this block's parent. best_block_number: BlockNumber, /// The head of the `ImportedBlockNumbers` ring buffer. 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: frame_system::Config { /// The upper bound on the number of requests allowed by the pallet. /// /// A request refers to an action which writes a header to storage. /// /// Once this bound is reached the pallet will reject all commitments /// until the request count has decreased. #[pallet::constant] type MaxRequests: Get; /// Maximal number of imported commitments to keep in the storage. /// /// The setting is there to prevent growing the on-chain state indefinitely. Note /// the setting does not relate to block numbers - we will simply keep as much items /// in the storage, so it doesn't guarantee any fixed timeframe for imported commitments. #[pallet::constant] type CommitmentsToKeep: Get; /// The chain we are bridging to here. type BridgedChain: ChainWithBeefy; } #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(PhantomData<(T, I)>); #[pallet::hooks] impl, I: 'static> Hooks> for Pallet { fn on_initialize(_n: BlockNumberFor) -> frame_support::weights::Weight { >::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, I: 'static> OwnedBridgeModule for Pallet { const LOG_TARGET: &'static str = LOG_TARGET; type OwnerStorage = PalletOwner; type OperatingMode = BasicOperatingMode; type OperatingModeStorage = PalletOperatingMode; } #[pallet::call] impl, I: 'static> Pallet where BridgedMmrHashing: 'static + Send + Sync, { /// Initialize pallet with BEEFY authority set and best known finalized block number. #[pallet::call_index(0)] #[pallet::weight((T::DbWeight::get().reads_writes(2, 3), DispatchClass::Operational))] pub fn initialize( origin: OriginFor, init_data: InitializationDataOf, ) -> DispatchResult { Self::ensure_owner_or_root(origin)?; let is_initialized = >::exists(); ensure!(!is_initialized, >::AlreadyInitialized); tracing::info!(target: LOG_TARGET, ?init_data, "Initializing bridge BEEFY pallet"); Ok(initialize::(init_data)?) } /// Change `PalletOwner`. /// /// May only be called either by root, or by `PalletOwner`. #[pallet::call_index(1)] #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] pub fn set_owner(origin: OriginFor, new_owner: Option) -> DispatchResult { >::set_owner(origin, new_owner) } /// Halt or resume all pallet operations. /// /// May only be called either by root, or by `PalletOwner`. #[pallet::call_index(2)] #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] pub fn set_operating_mode( origin: OriginFor, operating_mode: BasicOperatingMode, ) -> DispatchResult { >::set_operating_mode(origin, operating_mode) } /// Submit a commitment generated by BEEFY authority set. /// /// It will use the underlying storage pallet to fetch information about the current /// authority set and best finalized block number in order to verify that the commitment /// is valid. /// /// If successful in verification, it will update the underlying storage with the data /// provided in the newly submitted commitment. #[pallet::call_index(3)] #[pallet::weight(0)] pub fn submit_commitment( origin: OriginFor, commitment: BridgedBeefySignedCommitment, validator_set: BridgedBeefyAuthoritySet, mmr_leaf: Box>, mmr_proof: BridgedMmrProof, ) -> DispatchResult where BridgedBeefySignedCommitment: Clone, { Self::ensure_not_halted().map_err(Error::::BridgeModule)?; ensure_signed(origin)?; ensure!(Self::request_count() < T::MaxRequests::get(), >::TooManyRequests); // Ensure that the commitment is for a better block. let commitments_info = ImportedCommitmentsInfo::::get().ok_or(Error::::NotInitialized)?; ensure!( commitment.commitment.block_number > commitments_info.best_block_number, Error::::OldCommitment ); // Verify commitment and mmr leaf. let current_authority_set_info = CurrentAuthoritySetInfo::::get(); let mmr_root = utils::verify_commitment::( &commitment, ¤t_authority_set_info, &validator_set, )?; utils::verify_beefy_mmr_leaf::(&mmr_leaf, mmr_proof, mmr_root)?; // Update request count. RequestCount::::mutate(|count| *count += 1); // Update authority set if needed. if mmr_leaf.beefy_next_authority_set.id > current_authority_set_info.id { CurrentAuthoritySetInfo::::put(mmr_leaf.beefy_next_authority_set); } // Import commitment. let block_number_index = commitments_info.next_block_number_index; let to_prune = ImportedBlockNumbers::::try_get(block_number_index); ImportedCommitments::::insert( commitment.commitment.block_number, ImportedCommitment:: { parent_number_and_hash: mmr_leaf.parent_number_and_hash, mmr_root, }, ); ImportedBlockNumbers::::insert( block_number_index, commitment.commitment.block_number, ); ImportedCommitmentsInfo::::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 { tracing::debug!( target: LOG_TARGET, ?old_block_number, "Pruning commitment for old block." ); ImportedCommitments::::remove(old_block_number); } tracing::info!( target: LOG_TARGET, block=?commitment.commitment.block_number, "Successfully imported commitment for block", ); Ok(()) } } /// The current number of requests which have written to storage. /// /// If the `RequestCount` hits `MaxRequests`, no more calls will be allowed to the pallet until /// the request capacity is increased. /// /// The `RequestCount` is decreased by one at the beginning of every block. This is to ensure /// that the pallet can always make progress. #[pallet::storage] pub type RequestCount, I: 'static = ()> = StorageValue<_, u32, ValueQuery>; /// High level info about the imported commitments. /// /// Contains the following info: /// - best known block number of the bridged chain, finalized by BEEFY /// - the head of the `ImportedBlockNumbers` ring buffer #[pallet::storage] pub type ImportedCommitmentsInfo, I: 'static = ()> = StorageValue<_, ImportedCommitmentsInfoData>>; /// A ring buffer containing the block numbers of the commitments that we have imported, /// ordered by the insertion time. #[pallet::storage] pub(super) type ImportedBlockNumbers, I: 'static = ()> = StorageMap<_, Identity, u32, BridgedBlockNumber>; /// All the commitments that we have imported and haven't been pruned yet. #[pallet::storage] pub type ImportedCommitments, I: 'static = ()> = StorageMap<_, Blake2_128Concat, BridgedBlockNumber, ImportedCommitment>; /// The current BEEFY authority set at the bridged chain. #[pallet::storage] pub type CurrentAuthoritySetInfo, I: 'static = ()> = StorageValue<_, BridgedBeefyAuthoritySetInfo, ValueQuery>; /// Optional pallet owner. /// /// Pallet owner has the right to halt all pallet operations and then resume it. If it is /// `None`, then there are no direct ways to halt/resume pallet operations, but other /// runtime methods may still be used to do that (i.e. `democracy::referendum` to update halt /// flag directly or calling `set_operating_mode`). #[pallet::storage] pub type PalletOwner, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>; /// The current operating mode of the pallet. /// /// Depending on the mode either all, or no transactions will be allowed. #[pallet::storage] pub type PalletOperatingMode, I: 'static = ()> = StorageValue<_, BasicOperatingMode, ValueQuery>; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig, I: 'static = ()> { /// Optional module owner account. pub owner: Option, /// Optional module initialization data. pub init_data: Option>, } #[pallet::genesis_build] impl, I: 'static> BuildGenesisConfig for GenesisConfig { fn build(&self) { if let Some(ref owner) = self.owner { >::put(owner); } if let Some(init_data) = self.init_data.clone() { initialize::(init_data) .expect("invalid initialization data of BEEFY bridge pallet"); } else { // Since the bridge hasn't been initialized we shouldn't allow anyone to perform // transactions. >::put(BasicOperatingMode::Halted); } } } #[pallet::error] pub enum Error { /// The pallet has not been initialized yet. NotInitialized, /// The pallet has already been initialized. AlreadyInitialized, /// Invalid initial authority set. InvalidInitialAuthoritySet, /// There are too many requests for the current window to handle. TooManyRequests, /// The imported commitment is older than the best commitment known to the pallet. OldCommitment, /// The commitment is signed by unknown validator set. InvalidCommitmentValidatorSetId, /// The id of the provided validator set is invalid. InvalidValidatorSetId, /// The number of signatures in the commitment is invalid. InvalidCommitmentSignaturesLen, /// The number of validator ids provided is invalid. InvalidValidatorSetLen, /// There aren't enough correct signatures in the commitment to finalize the block. NotEnoughCorrectSignatures, /// MMR root is missing from the commitment. MmrRootMissingFromCommitment, /// MMR proof verification has failed. MmrProofVerificationFailed, /// The validators are not matching the merkle tree root of the authority set. InvalidValidatorSetRoot, /// Error generated by the `OwnedBridgeModule` trait. BridgeModule(bp_runtime::OwnedBridgeModuleError), } /// Initialize pallet with given parameters. pub(super) fn initialize, I: 'static>( init_data: InitializationDataOf, ) -> Result<(), Error> { if init_data.authority_set.len == 0 { return Err(Error::::InvalidInitialAuthoritySet); } CurrentAuthoritySetInfo::::put(init_data.authority_set); >::put(init_data.operating_mode); ImportedCommitmentsInfo::::put(ImportedCommitmentsInfoData { best_block_number: init_data.best_block_number, next_block_number_index: 0, }); Ok(()) } impl, I: 'static> Pallet { /// The current number of requests which have written to storage. pub fn request_count() -> u32 { RequestCount::::get() } } } #[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::::block_number(); frame_system::Pallet::::set_block_number(current_number + 1); let _ = Pallet::::on_initialize(current_number); } fn import_header_chain(headers: Vec) { 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::::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::::AlreadyInitialized, ); }); } #[test] fn fails_to_initialize_if_authority_set_is_empty() { run_test(|| { assert_noop!( Pallet::::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::::InvalidInitialAuthoritySet, ); }); } #[test] fn fails_to_import_commitment_if_halted() { run_test_with_initialize(1, || { assert_ok!(Pallet::::set_operating_mode( RuntimeOrigin::root(), BasicOperatingMode::Halted )); assert_noop!( import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()), Error::::BridgeModule(OwnedBridgeModuleError::Halted), ); }) } #[test] fn fails_to_import_commitment_if_too_many_requests() { run_test_with_initialize(1, || { let max_requests = <::MaxRequests as Get>::get() as u64; let mut chain = ChainBuilder::new(1); for _ in 0..max_requests + 2 { chain = chain.append_finalized_header(); } // import `max_request` headers for i in 0..max_requests { assert_ok!(import_commitment(chain.header(i + 1))); } // try to import next header: it fails because we are no longer accepting commitments assert_noop!( import_commitment(chain.header(max_requests + 1)), Error::::TooManyRequests, ); // when next block is "started", we allow import of next header next_block(); assert_ok!(import_commitment(chain.header(max_requests + 1))); // but we can't import two headers until next block and so on assert_noop!( import_commitment(chain.header(max_requests + 2)), Error::::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::::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) // 2..17 .append_finalized_header() // 18 .append_default_headers(16) // 19..34 .append_handoff_header(9) // 35 .append_default_headers(8) // 36..43 .append_finalized_header() // 44 .append_default_headers(8) // 45..52 .append_handoff_header(17) // 53 .append_default_headers(4) // 54..57 .append_finalized_header() // 58 .append_default_headers(4); // 59..63 import_header_chain(chain.to_chain()); assert_eq!( ImportedCommitmentsInfo::::get().unwrap().best_block_number, 58 ); assert_eq!(CurrentAuthoritySetInfo::::get().id, 2); assert_eq!(CurrentAuthoritySetInfo::::get().len, 17); let imported_commitment = ImportedCommitments::::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 = >::CommitmentsToKeep::get(); let commitments_to_import: Vec = ChainBuilder::new(3) .append_finalized_headers(commitments_to_keep as usize + 2) .to_chain(); // import exactly `CommitmentsToKeep` commitments for index in 0..commitments_to_keep { next_block(); import_commitment(commitments_to_import[index as usize].clone()) .expect("must succeed"); assert_eq!( ImportedCommitmentsInfo::::get().unwrap().next_block_number_index, (index + 1) % commitments_to_keep ); } // ensure that all commitments are in the storage assert_eq!( ImportedCommitmentsInfo::::get().unwrap().best_block_number, commitments_to_keep as TestBridgedBlockNumber ); assert_eq!( ImportedCommitmentsInfo::::get().unwrap().next_block_number_index, 0 ); for index in 0..commitments_to_keep { assert!(ImportedCommitments::::get( index as TestBridgedBlockNumber + 1 ) .is_some()); assert_eq!( ImportedBlockNumbers::::get(index), Some(Into::into(index + 1)), ); } // import next commitment next_block(); import_commitment(commitments_to_import[commitments_to_keep as usize].clone()) .expect("must succeed"); assert_eq!( ImportedCommitmentsInfo::::get().unwrap().next_block_number_index, 1 ); assert!(ImportedCommitments::::get( commitments_to_keep as TestBridgedBlockNumber + 1 ) .is_some()); assert_eq!( ImportedBlockNumbers::::get(0), Some(Into::into(commitments_to_keep + 1)), ); // the side effect of the import is that the commitment#1 is pruned assert!(ImportedCommitments::::get(1).is_none()); // import next commitment next_block(); import_commitment(commitments_to_import[commitments_to_keep as usize + 1].clone()) .expect("must succeed"); assert_eq!( ImportedCommitmentsInfo::::get().unwrap().next_block_number_index, 2 ); assert!(ImportedCommitments::::get( commitments_to_keep as TestBridgedBlockNumber + 2 ) .is_some()); assert_eq!( ImportedBlockNumbers::::get(1), Some(Into::into(commitments_to_keep + 2)), ); // the side effect of the import is that the commitment#2 is pruned assert!(ImportedCommitments::::get(1).is_none()); assert!(ImportedCommitments::::get(2).is_none()); }); } generate_owned_bridge_module_tests!(BasicOperatingMode::Normal, BasicOperatingMode::Halted); }