referrerpolicy=no-referrer-when-downgrade

pallet_bridge_beefy/
lib.rs

1// Copyright 2021 Parity Technologies (UK) Ltd.
2// This file is part of Parity Bridges Common.
3
4// Parity Bridges Common is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Parity Bridges Common is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
16
17//! BEEFY bridge pallet.
18//!
19//! This pallet is an on-chain BEEFY light client for Substrate-based chains that are using the
20//! following pallets bundle: `pallet-mmr`, `pallet-beefy` and `pallet-beefy-mmr`.
21//!
22//! The pallet is able to verify MMR leaf proofs and BEEFY commitments, so it has access
23//! to the following data of the bridged chain:
24//!
25//! - header hashes
26//! - changes of BEEFY authorities
27//! - extra data of MMR leafs
28//!
29//! Given the header hash, other pallets are able to verify header-based proofs
30//! (e.g. storage proofs, transaction inclusion proofs, etc.).
31
32#![warn(missing_docs)]
33#![cfg_attr(not(feature = "std"), no_std)]
34
35use bp_beefy::{ChainWithBeefy, InitializationData};
36use sp_std::{boxed::Box, prelude::*};
37
38// Re-export in crate namespace for `construct_runtime!`
39pub use pallet::*;
40
41mod utils;
42
43#[cfg(test)]
44mod mock;
45#[cfg(test)]
46mod mock_chain;
47
48/// The target that will be used when publishing logs related to this pallet.
49pub const LOG_TARGET: &str = "runtime::bridge-beefy";
50
51/// Configured bridged chain.
52pub type BridgedChain<T, I> = <T as Config<I>>::BridgedChain;
53/// Block number, used by configured bridged chain.
54pub type BridgedBlockNumber<T, I> = bp_runtime::BlockNumberOf<BridgedChain<T, I>>;
55/// Block hash, used by configured bridged chain.
56pub type BridgedBlockHash<T, I> = bp_runtime::HashOf<BridgedChain<T, I>>;
57
58/// Pallet initialization data.
59pub type InitializationDataOf<T, I> =
60	InitializationData<BridgedBlockNumber<T, I>, bp_beefy::MmrHashOf<BridgedChain<T, I>>>;
61/// BEEFY commitment hasher, used by configured bridged chain.
62pub type BridgedBeefyCommitmentHasher<T, I> = bp_beefy::BeefyCommitmentHasher<BridgedChain<T, I>>;
63/// BEEFY validator id, used by configured bridged chain.
64pub type BridgedBeefyAuthorityId<T, I> = bp_beefy::BeefyAuthorityIdOf<BridgedChain<T, I>>;
65/// BEEFY validator set, used by configured bridged chain.
66pub type BridgedBeefyAuthoritySet<T, I> = bp_beefy::BeefyAuthoritySetOf<BridgedChain<T, I>>;
67/// BEEFY authority set, used by configured bridged chain.
68pub type BridgedBeefyAuthoritySetInfo<T, I> = bp_beefy::BeefyAuthoritySetInfoOf<BridgedChain<T, I>>;
69/// BEEFY signed commitment, used by configured bridged chain.
70pub type BridgedBeefySignedCommitment<T, I> = bp_beefy::BeefySignedCommitmentOf<BridgedChain<T, I>>;
71/// MMR hashing algorithm, used by configured bridged chain.
72pub type BridgedMmrHashing<T, I> = bp_beefy::MmrHashingOf<BridgedChain<T, I>>;
73/// MMR hashing output type of `BridgedMmrHashing<T, I>`.
74pub type BridgedMmrHash<T, I> = bp_beefy::MmrHashOf<BridgedChain<T, I>>;
75/// The type of the MMR leaf extra data used by the configured bridged chain.
76pub type BridgedBeefyMmrLeafExtra<T, I> = bp_beefy::BeefyMmrLeafExtraOf<BridgedChain<T, I>>;
77/// BEEFY MMR proof type used by the pallet
78pub type BridgedMmrProof<T, I> = bp_beefy::MmrProofOf<BridgedChain<T, I>>;
79/// MMR leaf type, used by configured bridged chain.
80pub type BridgedBeefyMmrLeaf<T, I> = bp_beefy::BeefyMmrLeafOf<BridgedChain<T, I>>;
81/// Imported commitment data, stored by the pallet.
82pub type ImportedCommitment<T, I> = bp_beefy::ImportedCommitment<
83	BridgedBlockNumber<T, I>,
84	BridgedBlockHash<T, I>,
85	BridgedMmrHash<T, I>,
86>;
87
88/// Some high level info about the imported commitments.
89#[derive(codec::Encode, codec::Decode, scale_info::TypeInfo)]
90pub struct ImportedCommitmentsInfoData<BlockNumber> {
91	/// Best known block number, provided in a BEEFY commitment. However this is not
92	/// the best proven block. The best proven block is this block's parent.
93	best_block_number: BlockNumber,
94	/// The head of the `ImportedBlockNumbers` ring buffer.
95	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		/// The upper bound on the number of requests allowed by the pallet.
108		///
109		/// A request refers to an action which writes a header to storage.
110		///
111		/// Once this bound is reached the pallet will reject all commitments
112		/// until the request count has decreased.
113		#[pallet::constant]
114		type MaxRequests: Get<u32>;
115
116		/// Maximal number of imported commitments to keep in the storage.
117		///
118		/// The setting is there to prevent growing the on-chain state indefinitely. Note
119		/// the setting does not relate to block numbers - we will simply keep as much items
120		/// in the storage, so it doesn't guarantee any fixed timeframe for imported commitments.
121		#[pallet::constant]
122		type CommitmentsToKeep: Get<u32>;
123
124		/// The chain we are bridging to here.
125		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		/// Initialize pallet with BEEFY authority set and best known finalized block number.
156		#[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		/// Change `PalletOwner`.
172		///
173		/// May only be called either by root, or by `PalletOwner`.
174		#[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		/// Halt or resume all pallet operations.
181		///
182		/// May only be called either by root, or by `PalletOwner`.
183		#[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		/// Submit a commitment generated by BEEFY authority set.
193		///
194		/// It will use the underlying storage pallet to fetch information about the current
195		/// authority set and best finalized block number in order to verify that the commitment
196		/// is valid.
197		///
198		/// If successful in verification, it will update the underlying storage with the data
199		/// provided in the newly submitted commitment.
200		#[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			// Ensure that the commitment is for a better block.
218			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			// Verify commitment and mmr leaf.
226			let current_authority_set_info = CurrentAuthoritySetInfo::<T, I>::get();
227			let mmr_root = utils::verify_commitment::<T, I>(
228				&commitment,
229				&current_authority_set_info,
230				&validator_set,
231			)?;
232			utils::verify_beefy_mmr_leaf::<T, I>(&mmr_leaf, mmr_proof, mmr_root)?;
233
234			// Update request count.
235			RequestCount::<T, I>::mutate(|count| *count += 1);
236			// Update authority set if needed.
237			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			// Import commitment.
242			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	/// The current number of requests which have written to storage.
279	///
280	/// If the `RequestCount` hits `MaxRequests`, no more calls will be allowed to the pallet until
281	/// the request capacity is increased.
282	///
283	/// The `RequestCount` is decreased by one at the beginning of every block. This is to ensure
284	/// that the pallet can always make progress.
285	#[pallet::storage]
286	pub type RequestCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
287
288	/// High level info about the imported commitments.
289	///
290	/// Contains the following info:
291	/// - best known block number of the bridged chain, finalized by BEEFY
292	/// - the head of the `ImportedBlockNumbers` ring buffer
293	#[pallet::storage]
294	pub type ImportedCommitmentsInfo<T: Config<I>, I: 'static = ()> =
295		StorageValue<_, ImportedCommitmentsInfoData<BridgedBlockNumber<T, I>>>;
296
297	/// A ring buffer containing the block numbers of the commitments that we have imported,
298	/// ordered by the insertion time.
299	#[pallet::storage]
300	pub(super) type ImportedBlockNumbers<T: Config<I>, I: 'static = ()> =
301		StorageMap<_, Identity, u32, BridgedBlockNumber<T, I>>;
302
303	/// All the commitments that we have imported and haven't been pruned yet.
304	#[pallet::storage]
305	pub type ImportedCommitments<T: Config<I>, I: 'static = ()> =
306		StorageMap<_, Blake2_128Concat, BridgedBlockNumber<T, I>, ImportedCommitment<T, I>>;
307
308	/// The current BEEFY authority set at the bridged chain.
309	#[pallet::storage]
310	pub type CurrentAuthoritySetInfo<T: Config<I>, I: 'static = ()> =
311		StorageValue<_, BridgedBeefyAuthoritySetInfo<T, I>, ValueQuery>;
312
313	/// Optional pallet owner.
314	///
315	/// Pallet owner has the right to halt all pallet operations and then resume it. If it is
316	/// `None`, then there are no direct ways to halt/resume pallet operations, but other
317	/// runtime methods may still be used to do that (i.e. `democracy::referendum` to update halt
318	/// flag directly or calling `set_operating_mode`).
319	#[pallet::storage]
320	pub type PalletOwner<T: Config<I>, I: 'static = ()> =
321		StorageValue<_, T::AccountId, OptionQuery>;
322
323	/// The current operating mode of the pallet.
324	///
325	/// Depending on the mode either all, or no transactions will be allowed.
326	#[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		/// Optional module owner account.
334		pub owner: Option<T::AccountId>,
335		/// Optional module initialization data.
336		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				// Since the bridge hasn't been initialized we shouldn't allow anyone to perform
351				// transactions.
352				<PalletOperatingMode<T, I>>::put(BasicOperatingMode::Halted);
353			}
354		}
355	}
356
357	#[pallet::error]
358	pub enum Error<T, I = ()> {
359		/// The pallet has not been initialized yet.
360		NotInitialized,
361		/// The pallet has already been initialized.
362		AlreadyInitialized,
363		/// Invalid initial authority set.
364		InvalidInitialAuthoritySet,
365		/// There are too many requests for the current window to handle.
366		TooManyRequests,
367		/// The imported commitment is older than the best commitment known to the pallet.
368		OldCommitment,
369		/// The commitment is signed by unknown validator set.
370		InvalidCommitmentValidatorSetId,
371		/// The id of the provided validator set is invalid.
372		InvalidValidatorSetId,
373		/// The number of signatures in the commitment is invalid.
374		InvalidCommitmentSignaturesLen,
375		/// The number of validator ids provided is invalid.
376		InvalidValidatorSetLen,
377		/// There aren't enough correct signatures in the commitment to finalize the block.
378		NotEnoughCorrectSignatures,
379		/// MMR root is missing from the commitment.
380		MmrRootMissingFromCommitment,
381		/// MMR proof verification has failed.
382		MmrProofVerificationFailed,
383		/// The validators are not matching the merkle tree root of the authority set.
384		InvalidValidatorSetRoot,
385		/// Error generated by the `OwnedBridgeModule` trait.
386		BridgeModule(bp_runtime::OwnedBridgeModuleError),
387	}
388
389	/// Initialize pallet with given parameters.
390	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		/// The current number of requests which have written to storage.
409		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			// import `max_request` headers
508			for i in 0..max_requests {
509				assert_ok!(import_commitment(chain.header(i + 1)));
510			}
511
512			// try to import next header: it fails because we are no longer accepting commitments
513			assert_noop!(
514				import_commitment(chain.header(max_requests + 1)),
515				Error::<TestRuntime, ()>::TooManyRequests,
516			);
517
518			// when next block is "started", we allow import of next header
519			next_block();
520			assert_ok!(import_commitment(chain.header(max_requests + 1)));
521
522			// but we can't import two headers until next block and so on
523			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) // 2..17
546				.append_finalized_header() // 18
547				.append_default_headers(16) // 19..34
548				.append_handoff_header(9) // 35
549				.append_default_headers(8) // 36..43
550				.append_finalized_header() // 44
551				.append_default_headers(8) // 45..52
552				.append_handoff_header(17) // 53
553				.append_default_headers(4) // 54..57
554				.append_finalized_header() // 58
555				.append_default_headers(4); // 59..63
556			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			// import exactly `CommitmentsToKeep` commitments
585			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			// ensure that all commitments are in the storage
596			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			// import next commitment
616			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			// the side effect of the import is that the commitment#1 is pruned
632			assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
633
634			// import next commitment
635			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			// the side effect of the import is that the commitment#2 is pruned
651			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}