1#![warn(missing_docs)]
33#![cfg_attr(not(feature = "std"), no_std)]
34
35use bp_beefy::{ChainWithBeefy, InitializationData};
36use sp_std::{boxed::Box, prelude::*};
37
38pub use pallet::*;
40
41mod utils;
42
43#[cfg(test)]
44mod mock;
45#[cfg(test)]
46mod mock_chain;
47
48pub const LOG_TARGET: &str = "runtime::bridge-beefy";
50
51pub type BridgedChain<T, I> = <T as Config<I>>::BridgedChain;
53pub type BridgedBlockNumber<T, I> = bp_runtime::BlockNumberOf<BridgedChain<T, I>>;
55pub type BridgedBlockHash<T, I> = bp_runtime::HashOf<BridgedChain<T, I>>;
57
58pub type InitializationDataOf<T, I> =
60 InitializationData<BridgedBlockNumber<T, I>, bp_beefy::MmrHashOf<BridgedChain<T, I>>>;
61pub type BridgedBeefyCommitmentHasher<T, I> = bp_beefy::BeefyCommitmentHasher<BridgedChain<T, I>>;
63pub type BridgedBeefyAuthorityId<T, I> = bp_beefy::BeefyAuthorityIdOf<BridgedChain<T, I>>;
65pub type BridgedBeefyAuthoritySet<T, I> = bp_beefy::BeefyAuthoritySetOf<BridgedChain<T, I>>;
67pub type BridgedBeefyAuthoritySetInfo<T, I> = bp_beefy::BeefyAuthoritySetInfoOf<BridgedChain<T, I>>;
69pub type BridgedBeefySignedCommitment<T, I> = bp_beefy::BeefySignedCommitmentOf<BridgedChain<T, I>>;
71pub type BridgedMmrHashing<T, I> = bp_beefy::MmrHashingOf<BridgedChain<T, I>>;
73pub type BridgedMmrHash<T, I> = bp_beefy::MmrHashOf<BridgedChain<T, I>>;
75pub type BridgedBeefyMmrLeafExtra<T, I> = bp_beefy::BeefyMmrLeafExtraOf<BridgedChain<T, I>>;
77pub type BridgedMmrProof<T, I> = bp_beefy::MmrProofOf<BridgedChain<T, I>>;
79pub type BridgedBeefyMmrLeaf<T, I> = bp_beefy::BeefyMmrLeafOf<BridgedChain<T, I>>;
81pub type ImportedCommitment<T, I> = bp_beefy::ImportedCommitment<
83 BridgedBlockNumber<T, I>,
84 BridgedBlockHash<T, I>,
85 BridgedMmrHash<T, I>,
86>;
87
88#[derive(codec::Encode, codec::Decode, scale_info::TypeInfo)]
90pub struct ImportedCommitmentsInfoData<BlockNumber> {
91 best_block_number: BlockNumber,
94 next_block_number_index: u32,
96}
97
98#[frame_support::pallet(dev_mode)]
99pub mod pallet {
100 use super::*;
101 use bp_runtime::{BasicOperatingMode, OwnedBridgeModule};
102 use frame_support::pallet_prelude::*;
103 use frame_system::pallet_prelude::*;
104
105 #[pallet::config]
106 pub trait Config<I: 'static = ()>: frame_system::Config {
107 #[pallet::constant]
114 type MaxRequests: Get<u32>;
115
116 #[pallet::constant]
122 type CommitmentsToKeep: Get<u32>;
123
124 type BridgedChain: ChainWithBeefy;
126 }
127
128 #[pallet::pallet]
129 #[pallet::without_storage_info]
130 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
131
132 #[pallet::hooks]
133 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
134 fn on_initialize(_n: BlockNumberFor<T>) -> frame_support::weights::Weight {
135 <RequestCount<T, I>>::mutate(|count| *count = count.saturating_sub(1));
136
137 Weight::from_parts(0, 0)
138 .saturating_add(T::DbWeight::get().reads(1))
139 .saturating_add(T::DbWeight::get().writes(1))
140 }
141 }
142
143 impl<T: Config<I>, I: 'static> OwnedBridgeModule<T> for Pallet<T, I> {
144 const LOG_TARGET: &'static str = LOG_TARGET;
145 type OwnerStorage = PalletOwner<T, I>;
146 type OperatingMode = BasicOperatingMode;
147 type OperatingModeStorage = PalletOperatingMode<T, I>;
148 }
149
150 #[pallet::call]
151 impl<T: Config<I>, I: 'static> Pallet<T, I>
152 where
153 BridgedMmrHashing<T, I>: 'static + Send + Sync,
154 {
155 #[pallet::call_index(0)]
157 #[pallet::weight((T::DbWeight::get().reads_writes(2, 3), DispatchClass::Operational))]
158 pub fn initialize(
159 origin: OriginFor<T>,
160 init_data: InitializationDataOf<T, I>,
161 ) -> DispatchResult {
162 Self::ensure_owner_or_root(origin)?;
163
164 let is_initialized = <ImportedCommitmentsInfo<T, I>>::exists();
165 ensure!(!is_initialized, <Error<T, I>>::AlreadyInitialized);
166
167 tracing::info!(target: LOG_TARGET, ?init_data, "Initializing bridge BEEFY pallet");
168 Ok(initialize::<T, I>(init_data)?)
169 }
170
171 #[pallet::call_index(1)]
175 #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
176 pub fn set_owner(origin: OriginFor<T>, new_owner: Option<T::AccountId>) -> DispatchResult {
177 <Self as OwnedBridgeModule<_>>::set_owner(origin, new_owner)
178 }
179
180 #[pallet::call_index(2)]
184 #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
185 pub fn set_operating_mode(
186 origin: OriginFor<T>,
187 operating_mode: BasicOperatingMode,
188 ) -> DispatchResult {
189 <Self as OwnedBridgeModule<_>>::set_operating_mode(origin, operating_mode)
190 }
191
192 #[pallet::call_index(3)]
201 #[pallet::weight(0)]
202 pub fn submit_commitment(
203 origin: OriginFor<T>,
204 commitment: BridgedBeefySignedCommitment<T, I>,
205 validator_set: BridgedBeefyAuthoritySet<T, I>,
206 mmr_leaf: Box<BridgedBeefyMmrLeaf<T, I>>,
207 mmr_proof: BridgedMmrProof<T, I>,
208 ) -> DispatchResult
209 where
210 BridgedBeefySignedCommitment<T, I>: Clone,
211 {
212 Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
213 ensure_signed(origin)?;
214
215 ensure!(Self::request_count() < T::MaxRequests::get(), <Error<T, I>>::TooManyRequests);
216
217 let commitments_info =
219 ImportedCommitmentsInfo::<T, I>::get().ok_or(Error::<T, I>::NotInitialized)?;
220 ensure!(
221 commitment.commitment.block_number > commitments_info.best_block_number,
222 Error::<T, I>::OldCommitment
223 );
224
225 let current_authority_set_info = CurrentAuthoritySetInfo::<T, I>::get();
227 let mmr_root = utils::verify_commitment::<T, I>(
228 &commitment,
229 ¤t_authority_set_info,
230 &validator_set,
231 )?;
232 utils::verify_beefy_mmr_leaf::<T, I>(&mmr_leaf, mmr_proof, mmr_root)?;
233
234 RequestCount::<T, I>::mutate(|count| *count += 1);
236 if mmr_leaf.beefy_next_authority_set.id > current_authority_set_info.id {
238 CurrentAuthoritySetInfo::<T, I>::put(mmr_leaf.beefy_next_authority_set);
239 }
240
241 let block_number_index = commitments_info.next_block_number_index;
243 let to_prune = ImportedBlockNumbers::<T, I>::try_get(block_number_index);
244 ImportedCommitments::<T, I>::insert(
245 commitment.commitment.block_number,
246 ImportedCommitment::<T, I> {
247 parent_number_and_hash: mmr_leaf.parent_number_and_hash,
248 mmr_root,
249 },
250 );
251 ImportedBlockNumbers::<T, I>::insert(
252 block_number_index,
253 commitment.commitment.block_number,
254 );
255 ImportedCommitmentsInfo::<T, I>::put(ImportedCommitmentsInfoData {
256 best_block_number: commitment.commitment.block_number,
257 next_block_number_index: (block_number_index + 1) % T::CommitmentsToKeep::get(),
258 });
259 if let Ok(old_block_number) = to_prune {
260 tracing::debug!(
261 target: LOG_TARGET,
262 ?old_block_number,
263 "Pruning commitment for old block."
264 );
265 ImportedCommitments::<T, I>::remove(old_block_number);
266 }
267
268 tracing::info!(
269 target: LOG_TARGET,
270 block=?commitment.commitment.block_number,
271 "Successfully imported commitment for block",
272 );
273
274 Ok(())
275 }
276 }
277
278 #[pallet::storage]
286 pub type RequestCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
287
288 #[pallet::storage]
294 pub type ImportedCommitmentsInfo<T: Config<I>, I: 'static = ()> =
295 StorageValue<_, ImportedCommitmentsInfoData<BridgedBlockNumber<T, I>>>;
296
297 #[pallet::storage]
300 pub(super) type ImportedBlockNumbers<T: Config<I>, I: 'static = ()> =
301 StorageMap<_, Identity, u32, BridgedBlockNumber<T, I>>;
302
303 #[pallet::storage]
305 pub type ImportedCommitments<T: Config<I>, I: 'static = ()> =
306 StorageMap<_, Blake2_128Concat, BridgedBlockNumber<T, I>, ImportedCommitment<T, I>>;
307
308 #[pallet::storage]
310 pub type CurrentAuthoritySetInfo<T: Config<I>, I: 'static = ()> =
311 StorageValue<_, BridgedBeefyAuthoritySetInfo<T, I>, ValueQuery>;
312
313 #[pallet::storage]
320 pub type PalletOwner<T: Config<I>, I: 'static = ()> =
321 StorageValue<_, T::AccountId, OptionQuery>;
322
323 #[pallet::storage]
327 pub type PalletOperatingMode<T: Config<I>, I: 'static = ()> =
328 StorageValue<_, BasicOperatingMode, ValueQuery>;
329
330 #[pallet::genesis_config]
331 #[derive(frame_support::DefaultNoBound)]
332 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
333 pub owner: Option<T::AccountId>,
335 pub init_data: Option<InitializationDataOf<T, I>>,
337 }
338
339 #[pallet::genesis_build]
340 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
341 fn build(&self) {
342 if let Some(ref owner) = self.owner {
343 <PalletOwner<T, I>>::put(owner);
344 }
345
346 if let Some(init_data) = self.init_data.clone() {
347 initialize::<T, I>(init_data)
348 .expect("invalid initialization data of BEEFY bridge pallet");
349 } else {
350 <PalletOperatingMode<T, I>>::put(BasicOperatingMode::Halted);
353 }
354 }
355 }
356
357 #[pallet::error]
358 pub enum Error<T, I = ()> {
359 NotInitialized,
361 AlreadyInitialized,
363 InvalidInitialAuthoritySet,
365 TooManyRequests,
367 OldCommitment,
369 InvalidCommitmentValidatorSetId,
371 InvalidValidatorSetId,
373 InvalidCommitmentSignaturesLen,
375 InvalidValidatorSetLen,
377 NotEnoughCorrectSignatures,
379 MmrRootMissingFromCommitment,
381 MmrProofVerificationFailed,
383 InvalidValidatorSetRoot,
385 BridgeModule(bp_runtime::OwnedBridgeModuleError),
387 }
388
389 pub(super) fn initialize<T: Config<I>, I: 'static>(
391 init_data: InitializationDataOf<T, I>,
392 ) -> Result<(), Error<T, I>> {
393 if init_data.authority_set.len == 0 {
394 return Err(Error::<T, I>::InvalidInitialAuthoritySet);
395 }
396 CurrentAuthoritySetInfo::<T, I>::put(init_data.authority_set);
397
398 <PalletOperatingMode<T, I>>::put(init_data.operating_mode);
399 ImportedCommitmentsInfo::<T, I>::put(ImportedCommitmentsInfoData {
400 best_block_number: init_data.best_block_number,
401 next_block_number_index: 0,
402 });
403
404 Ok(())
405 }
406
407 impl<T: Config<I>, I: 'static> Pallet<T, I> {
408 pub fn request_count() -> u32 {
410 RequestCount::<T, I>::get()
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use bp_runtime::{BasicOperatingMode, OwnedBridgeModuleError};
419 use bp_test_utils::generate_owned_bridge_module_tests;
420 use frame_support::{assert_noop, assert_ok, traits::Get};
421 use mock::*;
422 use mock_chain::*;
423 use sp_consensus_beefy::mmr::BeefyAuthoritySet;
424 use sp_runtime::DispatchError;
425
426 fn next_block() {
427 use frame_support::traits::OnInitialize;
428
429 let current_number = frame_system::Pallet::<TestRuntime>::block_number();
430 frame_system::Pallet::<TestRuntime>::set_block_number(current_number + 1);
431 let _ = Pallet::<TestRuntime>::on_initialize(current_number);
432 }
433
434 fn import_header_chain(headers: Vec<HeaderAndCommitment>) {
435 for header in headers {
436 if header.commitment.is_some() {
437 assert_ok!(import_commitment(header));
438 }
439 }
440 }
441
442 #[test]
443 fn fails_to_initialize_if_already_initialized() {
444 run_test_with_initialize(32, || {
445 assert_noop!(
446 Pallet::<TestRuntime>::initialize(
447 RuntimeOrigin::root(),
448 InitializationData {
449 operating_mode: BasicOperatingMode::Normal,
450 best_block_number: 0,
451 authority_set: BeefyAuthoritySet {
452 id: 0,
453 len: 1,
454 keyset_commitment: [0u8; 32].into()
455 }
456 }
457 ),
458 Error::<TestRuntime, ()>::AlreadyInitialized,
459 );
460 });
461 }
462
463 #[test]
464 fn fails_to_initialize_if_authority_set_is_empty() {
465 run_test(|| {
466 assert_noop!(
467 Pallet::<TestRuntime>::initialize(
468 RuntimeOrigin::root(),
469 InitializationData {
470 operating_mode: BasicOperatingMode::Normal,
471 best_block_number: 0,
472 authority_set: BeefyAuthoritySet {
473 id: 0,
474 len: 0,
475 keyset_commitment: [0u8; 32].into()
476 }
477 }
478 ),
479 Error::<TestRuntime, ()>::InvalidInitialAuthoritySet,
480 );
481 });
482 }
483
484 #[test]
485 fn fails_to_import_commitment_if_halted() {
486 run_test_with_initialize(1, || {
487 assert_ok!(Pallet::<TestRuntime>::set_operating_mode(
488 RuntimeOrigin::root(),
489 BasicOperatingMode::Halted
490 ));
491 assert_noop!(
492 import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()),
493 Error::<TestRuntime, ()>::BridgeModule(OwnedBridgeModuleError::Halted),
494 );
495 })
496 }
497
498 #[test]
499 fn fails_to_import_commitment_if_too_many_requests() {
500 run_test_with_initialize(1, || {
501 let max_requests = <<TestRuntime as Config>::MaxRequests as Get<u32>>::get() as u64;
502 let mut chain = ChainBuilder::new(1);
503 for _ in 0..max_requests + 2 {
504 chain = chain.append_finalized_header();
505 }
506
507 for i in 0..max_requests {
509 assert_ok!(import_commitment(chain.header(i + 1)));
510 }
511
512 assert_noop!(
514 import_commitment(chain.header(max_requests + 1)),
515 Error::<TestRuntime, ()>::TooManyRequests,
516 );
517
518 next_block();
520 assert_ok!(import_commitment(chain.header(max_requests + 1)));
521
522 assert_noop!(
524 import_commitment(chain.header(max_requests + 2)),
525 Error::<TestRuntime, ()>::TooManyRequests,
526 );
527 })
528 }
529
530 #[test]
531 fn fails_to_import_commitment_if_not_initialized() {
532 run_test(|| {
533 assert_noop!(
534 import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()),
535 Error::<TestRuntime, ()>::NotInitialized,
536 );
537 })
538 }
539
540 #[test]
541 fn submit_commitment_works_with_long_chain_with_handoffs() {
542 run_test_with_initialize(3, || {
543 let chain = ChainBuilder::new(3)
544 .append_finalized_header()
545 .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());
557
558 assert_eq!(
559 ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number,
560 58
561 );
562 assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().id, 2);
563 assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().len, 17);
564
565 let imported_commitment = ImportedCommitments::<TestRuntime>::get(58).unwrap();
566 assert_eq!(
567 imported_commitment,
568 bp_beefy::ImportedCommitment {
569 parent_number_and_hash: (57, chain.header(57).header.hash()),
570 mmr_root: chain.header(58).mmr_root,
571 },
572 );
573 })
574 }
575
576 #[test]
577 fn commitment_pruning_works() {
578 run_test_with_initialize(3, || {
579 let commitments_to_keep = <TestRuntime as Config<()>>::CommitmentsToKeep::get();
580 let commitments_to_import: Vec<HeaderAndCommitment> = ChainBuilder::new(3)
581 .append_finalized_headers(commitments_to_keep as usize + 2)
582 .to_chain();
583
584 for index in 0..commitments_to_keep {
586 next_block();
587 import_commitment(commitments_to_import[index as usize].clone())
588 .expect("must succeed");
589 assert_eq!(
590 ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
591 (index + 1) % commitments_to_keep
592 );
593 }
594
595 assert_eq!(
597 ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number,
598 commitments_to_keep as TestBridgedBlockNumber
599 );
600 assert_eq!(
601 ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
602 0
603 );
604 for index in 0..commitments_to_keep {
605 assert!(ImportedCommitments::<TestRuntime>::get(
606 index as TestBridgedBlockNumber + 1
607 )
608 .is_some());
609 assert_eq!(
610 ImportedBlockNumbers::<TestRuntime>::get(index),
611 Some(Into::into(index + 1)),
612 );
613 }
614
615 next_block();
617 import_commitment(commitments_to_import[commitments_to_keep as usize].clone())
618 .expect("must succeed");
619 assert_eq!(
620 ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
621 1
622 );
623 assert!(ImportedCommitments::<TestRuntime>::get(
624 commitments_to_keep as TestBridgedBlockNumber + 1
625 )
626 .is_some());
627 assert_eq!(
628 ImportedBlockNumbers::<TestRuntime>::get(0),
629 Some(Into::into(commitments_to_keep + 1)),
630 );
631 assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
633
634 next_block();
636 import_commitment(commitments_to_import[commitments_to_keep as usize + 1].clone())
637 .expect("must succeed");
638 assert_eq!(
639 ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
640 2
641 );
642 assert!(ImportedCommitments::<TestRuntime>::get(
643 commitments_to_keep as TestBridgedBlockNumber + 2
644 )
645 .is_some());
646 assert_eq!(
647 ImportedBlockNumbers::<TestRuntime>::get(1),
648 Some(Into::into(commitments_to_keep + 2)),
649 );
650 assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
652 assert!(ImportedCommitments::<TestRuntime>::get(2).is_none());
653 });
654 }
655
656 generate_owned_bridge_module_tests!(BasicOperatingMode::Normal, BasicOperatingMode::Halted);
657}