referrerpolicy=no-referrer-when-downgrade

frame_support/
migrations.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use crate::{
19	defensive,
20	storage::{storage_prefix, transactional::with_transaction_opaque_err},
21	traits::{
22		Defensive, GetStorageVersion, NoStorageVersionSet, PalletInfoAccess, SafeMode,
23		StorageVersion,
24	},
25	weights::{RuntimeDbWeight, Weight, WeightMeter},
26};
27use alloc::vec::Vec;
28use codec::{Decode, Encode, MaxEncodedLen};
29use core::marker::PhantomData;
30use impl_trait_for_tuples::impl_for_tuples;
31use sp_arithmetic::traits::Bounded;
32use sp_core::Get;
33use sp_io::{hashing::twox_128, storage::clear_prefix, KillStorageResult};
34use sp_runtime::traits::Zero;
35
36/// Handles storage migration pallet versioning.
37///
38/// [`VersionedMigration`] allows developers to write migrations without worrying about checking and
39/// setting storage versions. Instead, the developer wraps their migration in this struct which
40/// takes care of version handling using best practices.
41///
42/// It takes 5 type parameters:
43/// - `From`: The version being upgraded from.
44/// - `To`: The version being upgraded to.
45/// - `Inner`: An implementation of `UncheckedOnRuntimeUpgrade`.
46/// - `Pallet`: The Pallet being upgraded.
47/// - `Weight`: The runtime's RuntimeDbWeight implementation.
48///
49/// When a [`VersionedMigration`] `on_runtime_upgrade`, `pre_upgrade`, or `post_upgrade` method is
50/// called, the on-chain version of the pallet is compared to `From`. If they match, the `Inner`
51/// `UncheckedOnRuntimeUpgrade` is called and the pallets on-chain version is set to `To`
52/// after the migration. Otherwise, a warning is logged notifying the developer that the upgrade was
53/// a noop and should probably be removed.
54///
55/// By not bounding `Inner` with `OnRuntimeUpgrade`, we prevent developers from
56/// accidentally using the unchecked version of the migration in a runtime upgrade instead of
57/// [`VersionedMigration`].
58///
59/// ### Examples
60/// ```ignore
61/// // In file defining migrations
62///
63/// /// Private module containing *version unchecked* migration logic.
64/// ///
65/// /// Should only be used by the [`VersionedMigration`] type in this module to create something to
66/// /// export.
67/// ///
68/// /// We keep this private so the unversioned migration cannot accidentally be used in any runtimes.
69/// ///
70/// /// For more about this pattern of keeping items private, see
71/// /// - https://github.com/rust-lang/rust/issues/30905
72/// /// - https://internals.rust-lang.org/t/lang-team-minutes-private-in-public-rules/4504/40
73/// mod version_unchecked {
74/// 	use super::*;
75/// 	pub struct VersionUncheckedMigrateV5ToV6<T>(core::marker::PhantomData<T>);
76/// 	impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV5ToV6<T> {
77/// 		// `UncheckedOnRuntimeUpgrade` implementation...
78/// 	}
79/// }
80///
81/// pub type MigrateV5ToV6<T, I> =
82/// 	VersionedMigration<
83/// 		5,
84/// 		6,
85/// 		VersionUncheckedMigrateV5ToV6<T, I>,
86/// 		crate::pallet::Pallet<T, I>,
87/// 		<T as frame_system::Config>::DbWeight
88/// 	>;
89///
90/// // Migrations tuple to pass to the Executive pallet:
91/// pub type Migrations = (
92/// 	// other migrations...
93/// 	MigrateV5ToV6<T, ()>,
94/// 	// other migrations...
95/// );
96/// ```
97pub struct VersionedMigration<const FROM: u16, const TO: u16, Inner, Pallet, Weight> {
98	_marker: PhantomData<(Inner, Pallet, Weight)>,
99}
100
101/// A helper enum to wrap the pre_upgrade bytes like an Option before passing them to post_upgrade.
102/// This enum is used rather than an Option to make the API clearer to the developer.
103#[derive(Encode, Decode)]
104pub enum VersionedPostUpgradeData {
105	/// The migration ran, inner vec contains pre_upgrade data.
106	MigrationExecuted(alloc::vec::Vec<u8>),
107	/// This migration is a noop, do not run post_upgrade checks.
108	Noop,
109}
110
111/// Implementation of the `OnRuntimeUpgrade` trait for `VersionedMigration`.
112///
113/// Its main function is to perform the runtime upgrade in `on_runtime_upgrade` only if the on-chain
114/// version of the pallets storage matches `From`, and after the upgrade set the on-chain storage to
115/// `To`. If the versions do not match, it writes a log notifying the developer that the migration
116/// is a noop.
117impl<
118		const FROM: u16,
119		const TO: u16,
120		Inner: crate::traits::UncheckedOnRuntimeUpgrade,
121		Pallet: GetStorageVersion<InCodeStorageVersion = StorageVersion> + PalletInfoAccess,
122		DbWeight: Get<RuntimeDbWeight>,
123	> crate::traits::OnRuntimeUpgrade for VersionedMigration<FROM, TO, Inner, Pallet, DbWeight>
124{
125	/// Executes pre_upgrade if the migration will run, and wraps the pre_upgrade bytes in
126	/// [`VersionedPostUpgradeData`] before passing them to post_upgrade, so it knows whether the
127	/// migration ran or not.
128	#[cfg(feature = "try-runtime")]
129	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
130		let on_chain_version = Pallet::on_chain_storage_version();
131		if on_chain_version == FROM {
132			Ok(VersionedPostUpgradeData::MigrationExecuted(Inner::pre_upgrade()?).encode())
133		} else {
134			Ok(VersionedPostUpgradeData::Noop.encode())
135		}
136	}
137
138	/// Executes the versioned runtime upgrade.
139	///
140	/// First checks if the pallets on-chain storage version matches the version of this upgrade. If
141	/// it matches, it calls `Inner::on_runtime_upgrade`, updates the on-chain version, and returns
142	/// the weight. If it does not match, it writes a log notifying the developer that the migration
143	/// is a noop.
144	fn on_runtime_upgrade() -> Weight {
145		let on_chain_version = Pallet::on_chain_storage_version();
146		if on_chain_version == FROM {
147			log::info!(
148				"๐Ÿšš Pallet {:?} VersionedMigration migrating storage version from {:?} to {:?}.",
149				Pallet::name(),
150				FROM,
151				TO
152			);
153
154			// Execute the migration
155			let weight = Inner::on_runtime_upgrade();
156
157			// Update the on-chain version
158			StorageVersion::new(TO).put::<Pallet>();
159
160			weight.saturating_add(DbWeight::get().reads_writes(1, 1))
161		} else {
162			log::warn!(
163				"๐Ÿšš Pallet {:?} VersionedMigration migration {}->{} can be removed; on-chain is already at {:?}.",
164				Pallet::name(),
165				FROM,
166				TO,
167				on_chain_version
168			);
169			DbWeight::get().reads(1)
170		}
171	}
172
173	/// Executes `Inner::post_upgrade` if the migration just ran.
174	///
175	/// pre_upgrade passes [`VersionedPostUpgradeData::MigrationExecuted`] to post_upgrade if
176	/// the migration ran, and [`VersionedPostUpgradeData::Noop`] otherwise.
177	#[cfg(feature = "try-runtime")]
178	fn post_upgrade(
179		versioned_post_upgrade_data_bytes: alloc::vec::Vec<u8>,
180	) -> Result<(), sp_runtime::TryRuntimeError> {
181		use codec::DecodeAll;
182		match <VersionedPostUpgradeData>::decode_all(&mut &versioned_post_upgrade_data_bytes[..])
183			.map_err(|_| "VersionedMigration post_upgrade failed to decode PreUpgradeData")?
184		{
185			VersionedPostUpgradeData::MigrationExecuted(inner_bytes) => {
186				Inner::post_upgrade(inner_bytes)
187			},
188			VersionedPostUpgradeData::Noop => Ok(()),
189		}
190	}
191}
192
193/// Can store the in-code pallet version on-chain.
194pub trait StoreInCodeStorageVersion<T: GetStorageVersion + PalletInfoAccess> {
195	/// Write the in-code storage version on-chain.
196	fn store_in_code_storage_version();
197}
198
199impl<T: GetStorageVersion<InCodeStorageVersion = StorageVersion> + PalletInfoAccess>
200	StoreInCodeStorageVersion<T> for StorageVersion
201{
202	fn store_in_code_storage_version() {
203		let version = <T as GetStorageVersion>::in_code_storage_version();
204		version.put::<T>();
205	}
206}
207
208impl<T: GetStorageVersion<InCodeStorageVersion = NoStorageVersionSet> + PalletInfoAccess>
209	StoreInCodeStorageVersion<T> for NoStorageVersionSet
210{
211	fn store_in_code_storage_version() {
212		StorageVersion::default().put::<T>();
213	}
214}
215
216/// Trait used by [`migrate_from_pallet_version_to_storage_version`] to do the actual migration.
217pub trait PalletVersionToStorageVersionHelper {
218	fn migrate(db_weight: &RuntimeDbWeight) -> Weight;
219}
220
221impl<T: GetStorageVersion + PalletInfoAccess> PalletVersionToStorageVersionHelper for T
222where
223	T::InCodeStorageVersion: StoreInCodeStorageVersion<T>,
224{
225	fn migrate(db_weight: &RuntimeDbWeight) -> Weight {
226		const PALLET_VERSION_STORAGE_KEY_POSTFIX: &[u8] = b":__PALLET_VERSION__:";
227
228		fn pallet_version_key(name: &str) -> [u8; 32] {
229			crate::storage::storage_prefix(name.as_bytes(), PALLET_VERSION_STORAGE_KEY_POSTFIX)
230		}
231
232		sp_io::storage::clear(&pallet_version_key(<T as PalletInfoAccess>::name()));
233
234		<T::InCodeStorageVersion as StoreInCodeStorageVersion<T>>::store_in_code_storage_version();
235
236		db_weight.writes(2)
237	}
238}
239
240#[cfg_attr(all(not(feature = "tuples-96"), not(feature = "tuples-128")), impl_for_tuples(64))]
241#[cfg_attr(all(feature = "tuples-96", not(feature = "tuples-128")), impl_for_tuples(96))]
242#[cfg_attr(feature = "tuples-128", impl_for_tuples(128))]
243impl PalletVersionToStorageVersionHelper for T {
244	fn migrate(db_weight: &RuntimeDbWeight) -> Weight {
245		let mut weight = Weight::zero();
246
247		for_tuples!( #( weight = weight.saturating_add(T::migrate(db_weight)); )* );
248
249		weight
250	}
251}
252
253/// Migrate from the `PalletVersion` struct to the new [`StorageVersion`] struct.
254///
255/// This will remove all `PalletVersion's` from the state and insert the in-code storage version.
256pub fn migrate_from_pallet_version_to_storage_version<
257	Pallets: PalletVersionToStorageVersionHelper,
258>(
259	db_weight: &RuntimeDbWeight,
260) -> Weight {
261	Pallets::migrate(db_weight)
262}
263
264/// `RemovePallet` is a utility struct used to remove all storage items associated with a specific
265/// pallet.
266///
267/// This struct is generic over two parameters:
268/// - `P` is a type that implements the `Get` trait for a static string, representing the pallet's
269///   name.
270/// - `DbWeight` is a type that implements the `Get` trait for `RuntimeDbWeight`, providing the
271///   weight for database operations.
272///
273/// On runtime upgrade, the `on_runtime_upgrade` function will clear all storage items associated
274/// with the specified pallet, logging the number of keys removed. If the `try-runtime` feature is
275/// enabled, the `pre_upgrade` and `post_upgrade` functions can be used to verify the storage
276/// removal before and after the upgrade.
277///
278/// # Examples:
279/// ```ignore
280/// construct_runtime! {
281/// 	pub enum Runtime
282/// 	{
283/// 		System: frame_system = 0,
284///
285/// 		SomePalletToRemove: pallet_something = 1,
286/// 		AnotherPalletToRemove: pallet_something_else = 2,
287///
288/// 		YourOtherPallets...
289/// 	}
290/// };
291///
292/// parameter_types! {
293/// 		pub const SomePalletToRemoveStr: &'static str = "SomePalletToRemove";
294/// 		pub const AnotherPalletToRemoveStr: &'static str = "AnotherPalletToRemove";
295/// }
296///
297/// pub type Migrations = (
298/// 	RemovePallet<SomePalletToRemoveStr, RocksDbWeight>,
299/// 	RemovePallet<AnotherPalletToRemoveStr, RocksDbWeight>,
300/// 	AnyOtherMigrations...
301/// );
302///
303/// impl frame_system::Config for Runtime {
304/// 	type SingleBlockMigrations = Migrations;
305/// }
306/// ```
307///
308/// WARNING: `RemovePallet` has no guard rails preventing it from bricking the chain if the
309/// operation of removing storage for the given pallet would exceed the block weight limit.
310///
311/// If your pallet has too many keys to be removed in a single block, it is advised to wait for
312/// a multi-block scheduler currently under development which will allow for removal of storage
313/// items (and performing other heavy migrations) over multiple blocks
314/// (see <https://github.com/paritytech/substrate/issues/13690>).
315pub struct RemovePallet<P: Get<&'static str>, DbWeight: Get<RuntimeDbWeight>>(
316	PhantomData<(P, DbWeight)>,
317);
318impl<P: Get<&'static str>, DbWeight: Get<RuntimeDbWeight>> frame_support::traits::OnRuntimeUpgrade
319	for RemovePallet<P, DbWeight>
320{
321	fn on_runtime_upgrade() -> frame_support::weights::Weight {
322		let hashed_prefix = twox_128(P::get().as_bytes());
323		let keys_removed = match clear_prefix(&hashed_prefix, None) {
324			KillStorageResult::AllRemoved(value) => value,
325			KillStorageResult::SomeRemaining(value) => {
326				log::error!(
327					"`clear_prefix` failed to remove all keys for {}. THIS SHOULD NEVER HAPPEN! ๐Ÿšจ",
328					P::get()
329				);
330				value
331			},
332		} as u64;
333
334		log::info!("Removed {} {} keys ๐Ÿงน", keys_removed, P::get());
335
336		DbWeight::get().reads_writes(keys_removed + 1, keys_removed)
337	}
338
339	#[cfg(feature = "try-runtime")]
340	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
341		use crate::storage::unhashed::contains_prefixed_key;
342
343		let hashed_prefix = twox_128(P::get().as_bytes());
344		match contains_prefixed_key(&hashed_prefix) {
345			true => log::info!("Found {} keys pre-removal ๐Ÿ‘€", P::get()),
346			false => log::warn!(
347				"Migration RemovePallet<{}> can be removed (no keys found pre-removal).",
348				P::get()
349			),
350		};
351		Ok(alloc::vec::Vec::new())
352	}
353
354	#[cfg(feature = "try-runtime")]
355	fn post_upgrade(_state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
356		use crate::storage::unhashed::contains_prefixed_key;
357
358		let hashed_prefix = twox_128(P::get().as_bytes());
359		match contains_prefixed_key(&hashed_prefix) {
360			true => {
361				log::error!("{} has keys remaining post-removal โ—", P::get());
362				return Err("Keys remaining post-removal, this should never happen ๐Ÿšจ".into());
363			},
364			false => log::info!("No {} keys found post-removal ๐ŸŽ‰", P::get()),
365		};
366		Ok(())
367	}
368}
369
370/// `RemoveStorage` is a utility struct used to remove a storage item from a specific pallet.
371///
372/// This struct is generic over three parameters:
373/// - `P` is a type that implements the [`Get`] trait for a static string, representing the pallet's
374///   name.
375/// - `S` is a type that implements the [`Get`] trait for a static string, representing the storage
376///   name.
377/// - `DbWeight` is a type that implements the [`Get`] trait for [`RuntimeDbWeight`], providing the
378///   weight for database operations.
379///
380/// On runtime upgrade, the `on_runtime_upgrade` function will clear the storage from the specified
381/// storage, logging the number of keys removed. If the `try-runtime` feature is enabled, the
382/// `pre_upgrade` and `post_upgrade` functions can be used to verify the storage removal before and
383/// after the upgrade.
384///
385/// # Examples:
386/// ```ignore
387/// construct_runtime! {
388/// 	pub enum Runtime
389/// 	{
390/// 		System: frame_system = 0,
391///
392/// 		SomePallet: pallet_something = 1,
393///
394/// 		YourOtherPallets...
395/// 	}
396/// };
397///
398/// parameter_types! {
399/// 		pub const SomePallet: &'static str = "SomePallet";
400/// 		pub const StorageAccounts: &'static str = "Accounts";
401/// 		pub const StorageAccountCount: &'static str = "AccountCount";
402/// }
403///
404/// pub type Migrations = (
405/// 	RemoveStorage<SomePallet, StorageAccounts, RocksDbWeight>,
406/// 	RemoveStorage<SomePallet, StorageAccountCount, RocksDbWeight>,
407/// 	AnyOtherMigrations...
408/// );
409///
410/// impl frame_system::Config for Runtime {
411/// 	type SingleBlockMigrations = Migrations;
412/// }
413/// ```
414///
415/// WARNING: `RemoveStorage` has no guard rails preventing it from bricking the chain if the
416/// operation of removing storage for the given pallet would exceed the block weight limit.
417///
418/// If your storage has too many keys to be removed in a single block, it is advised to wait for
419/// a multi-block scheduler currently under development which will allow for removal of storage
420/// items (and performing other heavy migrations) over multiple blocks
421/// (see <https://github.com/paritytech/substrate/issues/13690>).
422pub struct RemoveStorage<P: Get<&'static str>, S: Get<&'static str>, DbWeight: Get<RuntimeDbWeight>>(
423	PhantomData<(P, S, DbWeight)>,
424);
425impl<P: Get<&'static str>, S: Get<&'static str>, DbWeight: Get<RuntimeDbWeight>>
426	frame_support::traits::OnRuntimeUpgrade for RemoveStorage<P, S, DbWeight>
427{
428	fn on_runtime_upgrade() -> frame_support::weights::Weight {
429		let hashed_prefix = storage_prefix(P::get().as_bytes(), S::get().as_bytes());
430		let keys_removed = match clear_prefix(&hashed_prefix, None) {
431			KillStorageResult::AllRemoved(value) => value,
432			KillStorageResult::SomeRemaining(value) => {
433				log::error!(
434					"`clear_prefix` failed to remove all keys for storage `{}` from pallet `{}`. THIS SHOULD NEVER HAPPEN! ๐Ÿšจ",
435					S::get(), P::get()
436				);
437				value
438			},
439		} as u64;
440
441		log::info!("Removed `{}` `{}` `{}` keys ๐Ÿงน", keys_removed, P::get(), S::get());
442
443		DbWeight::get().reads_writes(keys_removed + 1, keys_removed)
444	}
445
446	#[cfg(feature = "try-runtime")]
447	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
448		use crate::storage::unhashed::contains_prefixed_key;
449
450		let hashed_prefix = storage_prefix(P::get().as_bytes(), S::get().as_bytes());
451		match contains_prefixed_key(&hashed_prefix) {
452			true => log::info!("Found `{}` `{}` keys pre-removal ๐Ÿ‘€", P::get(), S::get()),
453			false => log::warn!(
454				"Migration RemoveStorage<{}, {}> can be removed (no keys found pre-removal).",
455				P::get(),
456				S::get()
457			),
458		};
459		Ok(Default::default())
460	}
461
462	#[cfg(feature = "try-runtime")]
463	fn post_upgrade(_state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
464		use crate::storage::unhashed::contains_prefixed_key;
465
466		let hashed_prefix = storage_prefix(P::get().as_bytes(), S::get().as_bytes());
467		match contains_prefixed_key(&hashed_prefix) {
468			true => {
469				log::error!("`{}` `{}` has keys remaining post-removal โ—", P::get(), S::get());
470				return Err("Keys remaining post-removal, this should never happen ๐Ÿšจ".into());
471			},
472			false => log::info!("No `{}` `{}` keys found post-removal ๐ŸŽ‰", P::get(), S::get()),
473		};
474		Ok(())
475	}
476}
477
478/// A migration that can proceed in multiple steps.
479pub trait SteppedMigration {
480	/// The cursor type that stores the progress (aka. state) of this migration.
481	type Cursor: codec::FullCodec + codec::MaxEncodedLen;
482
483	/// The unique identifier type of this migration.
484	type Identifier: codec::FullCodec + codec::MaxEncodedLen;
485
486	/// The unique identifier of this migration.
487	///
488	/// If two migrations have the same identifier, then they are assumed to be identical.
489	fn id() -> Self::Identifier;
490
491	/// The maximum number of steps that this migration can take.
492	///
493	/// This can be used to enforce progress and prevent migrations becoming stuck forever. A
494	/// migration that exceeds its max steps is treated as failed. `None` means that there is no
495	/// limit.
496	fn max_steps() -> Option<u32> {
497		None
498	}
499
500	/// Returns the prefixes of the keys to be migrated or `None` if the migration does not know.
501	///
502	/// This function is optional and provides information to the migration framework
503	/// to know which storage prefixes are being migrated. It can be helpful to let
504	/// chain explorers know which part of the state is possibly in a state where it
505	/// cannot be read correctly.
506	fn migrating_prefixes() -> Option<impl IntoIterator<Item = Vec<u8>>> {
507		None::<core::iter::Empty<_>>
508	}
509
510	/// Try to migrate as much as possible with the given weight.
511	///
512	/// **ANY STORAGE CHANGES MUST BE ROLLED-BACK BY THE CALLER UPON ERROR.** This is necessary
513	/// since the caller cannot return a cursor in the error case. [`Self::transactional_step`] is
514	/// provided as convenience for a caller. A cursor of `None` implies that the migration is at
515	/// its end. A migration that once returned `Nonce` is guaranteed to never be called again.
516	fn step(
517		cursor: Option<Self::Cursor>,
518		meter: &mut WeightMeter,
519	) -> Result<Option<Self::Cursor>, SteppedMigrationError>;
520
521	/// Same as [`Self::step`], but rolls back pending changes in the error case.
522	fn transactional_step(
523		mut cursor: Option<Self::Cursor>,
524		meter: &mut WeightMeter,
525	) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
526		with_transaction_opaque_err(move || match Self::step(cursor, meter) {
527			Ok(new_cursor) => {
528				cursor = new_cursor;
529				sp_runtime::TransactionOutcome::Commit(Ok(cursor))
530			},
531			Err(err) => sp_runtime::TransactionOutcome::Rollback(Err(err)),
532		})
533		.map_err(|()| SteppedMigrationError::Failed)?
534	}
535
536	/// Hook for testing that is run before the migration is started.
537	///
538	/// Returns some bytes which are passed into `post_upgrade` after the migration is completed.
539	/// This is not run for the real migration, so panicking is not an issue here.
540	#[cfg(feature = "try-runtime")]
541	fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
542		Ok(Vec::new())
543	}
544
545	/// Hook for testing that is run after the migration is completed.
546	///
547	/// Should be used to verify the state of the chain after the migration. The `state` parameter
548	/// is the return value from `pre_upgrade`. This is not run for the real migration, so panicking
549	/// is not an issue here.
550	#[cfg(feature = "try-runtime")]
551	fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
552		Ok(())
553	}
554}
555
556/// Error that can occur during a [`SteppedMigration`].
557#[derive(Debug, Encode, Decode, MaxEncodedLen, PartialEq, Eq, scale_info::TypeInfo)]
558pub enum SteppedMigrationError {
559	// Transient errors:
560	/// The remaining weight is not enough to do anything.
561	///
562	/// Can be resolved by calling with at least `required` weight. Note that calling it with
563	/// exactly `required` weight could cause it to not make any progress.
564	InsufficientWeight {
565		/// Amount of weight required to make progress.
566		required: Weight,
567	},
568	// Permanent errors:
569	/// The migration cannot decode its cursor and therefore not proceed.
570	///
571	/// This should not happen unless (1) the migration itself returned an invalid cursor in a
572	/// previous iteration, (2) the storage got corrupted or (3) there is a bug in the caller's
573	/// code.
574	InvalidCursor,
575	/// The migration encountered a permanent error and cannot continue.
576	Failed,
577}
578
579/// A generic migration identifier that can be used by MBMs.
580///
581/// It is not required that migrations use this identifier type, but it can help.
582#[derive(MaxEncodedLen, Encode, Decode)]
583pub struct MigrationId<const N: usize> {
584	pub pallet_id: [u8; N],
585	pub version_from: u8,
586	pub version_to: u8,
587}
588
589/// Notification handler for status updates regarding Multi-Block-Migrations.
590#[impl_trait_for_tuples::impl_for_tuples(8)]
591pub trait MigrationStatusHandler {
592	/// Notifies of the start of a runtime migration.
593	fn started() {}
594
595	/// Notifies of the completion of a runtime migration.
596	fn completed() {}
597}
598
599/// Handles a failed runtime migration.
600///
601/// This should never happen, but is here for completeness.
602pub trait FailedMigrationHandler {
603	/// Infallibly handle a failed runtime migration.
604	///
605	/// Gets passed in the optional index of the migration in the batch that caused the failure.
606	/// Returning `None` means that no automatic handling should take place and the callee decides
607	/// in the implementation what to do.
608	fn failed(migration: Option<u32>) -> FailedMigrationHandling;
609}
610
611/// Block any transactions to be processed after a migration failed.
612///
613/// This is **not a sane default**, since it prevents governance intervention.
614pub struct FreezeChainOnFailedMigration;
615
616impl FailedMigrationHandler for FreezeChainOnFailedMigration {
617	fn failed(_migration: Option<u32>) -> FailedMigrationHandling {
618		FailedMigrationHandling::KeepStuck
619	}
620}
621
622/// Ignore any MBM errors and unlock all calls that were locked during migration.
623///
624/// This implies that any storage invariants that were violated by a faulty MBM could now be exposed
625/// to users via calls. It is equivalent to how a faulty single-block-migration would be handled.
626pub struct ForceUnstuckOnFailedMigration;
627
628impl frame_support::migrations::FailedMigrationHandler for ForceUnstuckOnFailedMigration {
629	fn failed(_migration: Option<u32>) -> FailedMigrationHandling {
630		FailedMigrationHandling::ForceUnstuck
631	}
632}
633
634/// Enter safe mode on a failed runtime upgrade.
635///
636/// This can be very useful to manually intervene and fix the chain state. `Else` is used in case
637/// that the safe mode could not be entered.
638pub struct EnterSafeModeOnFailedMigration<SM, Else: FailedMigrationHandler>(
639	PhantomData<(SM, Else)>,
640);
641
642impl<Else: FailedMigrationHandler, SM: SafeMode> FailedMigrationHandler
643	for EnterSafeModeOnFailedMigration<SM, Else>
644where
645	<SM as SafeMode>::BlockNumber: Bounded,
646{
647	fn failed(migration: Option<u32>) -> FailedMigrationHandling {
648		let entered = if SM::is_entered() {
649			SM::extend(Bounded::max_value())
650		} else {
651			SM::enter(Bounded::max_value())
652		};
653
654		// If we could not enter or extend safe mode (for whatever reason), then we try the next.
655		if entered.is_err() {
656			Else::failed(migration)
657		} else {
658			FailedMigrationHandling::KeepStuck
659		}
660	}
661}
662
663/// How to proceed after a runtime upgrade failed.
664///
665/// There is NO SANE DEFAULT HERE. All options are very dangerous and should be used with care.
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
667pub enum FailedMigrationHandling {
668	/// Resume extrinsic processing of the chain. This will not resume the upgrade.
669	///
670	/// This should be supplemented with additional measures to ensure that the broken chain state
671	/// does not get further messed up by user extrinsics.
672	ForceUnstuck,
673	/// Set the cursor to `Stuck` and keep blocking extrinsics.
674	KeepStuck,
675	/// Don't do anything with the cursor and let the handler decide.
676	///
677	/// This can be useful in cases where the other two options would overwrite any changes that
678	/// were done by the handler to the cursor. If a handler returns this variant but does nothing,
679	/// then the chain will be stuck in an indefinite "crash loop" and keep emitting
680	/// `UpgradeFailed` events.
681	Ignore,
682}
683
684/// Something that can do multi step migrations.
685pub trait MultiStepMigrator {
686	/// Hint for whether [`Self::step`] should be called.
687	fn ongoing() -> bool;
688
689	/// Do the next step in the MBM process.
690	///
691	/// Must gracefully handle the case that it is currently not upgrading.
692	fn step() -> Weight;
693}
694
695impl MultiStepMigrator for () {
696	fn ongoing() -> bool {
697		false
698	}
699
700	fn step() -> Weight {
701		Weight::zero()
702	}
703}
704
705/// Multiple [`SteppedMigration`].
706pub trait SteppedMigrations {
707	/// The number of migrations that `Self` aggregates.
708	fn len() -> u32;
709
710	/// The `n`th [`SteppedMigration::id`].
711	///
712	/// Is guaranteed to return `Some` if `n < Self::len()`. Calling this with any index larger or
713	/// equal to `Self::len()` MUST return `None`.
714	fn nth_id(n: u32) -> Option<Vec<u8>>;
715
716	/// The [`SteppedMigration::max_steps`] of the `n`th migration.
717	///
718	/// Is guaranteed to return `Some` if `n < Self::len()`.
719	fn nth_max_steps(n: u32) -> Option<Option<u32>>;
720
721	/// Do a [`SteppedMigration::step`] on the `n`th migration.
722	///
723	/// Is guaranteed to return `Some` if `n < Self::len()`.
724	fn nth_step(
725		n: u32,
726		cursor: Option<Vec<u8>>,
727		meter: &mut WeightMeter,
728	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>>;
729
730	/// Do a [`SteppedMigration::transactional_step`] on the `n`th migration.
731	///
732	/// Is guaranteed to return `Some` if `n < Self::len()`.
733	fn nth_transactional_step(
734		n: u32,
735		cursor: Option<Vec<u8>>,
736		meter: &mut WeightMeter,
737	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>>;
738
739	/// Get the storage prefixes modified by the `n`th migration.
740	///
741	/// Returns `None` if the index is out of bounds.
742	fn nth_migrating_prefixes(n: u32) -> Option<Option<Vec<Vec<u8>>>>;
743
744	/// Call the pre-upgrade hooks of the `n`th migration.
745	///
746	/// Returns `None` if the index is out of bounds.
747	#[cfg(feature = "try-runtime")]
748	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>>;
749
750	/// Call the post-upgrade hooks of the `n`th migration.
751	///
752	/// Returns `None` if the index is out of bounds.
753	#[cfg(feature = "try-runtime")]
754	fn nth_post_upgrade(n: u32, _state: Vec<u8>)
755		-> Option<Result<(), sp_runtime::TryRuntimeError>>;
756
757	/// The maximal encoded length across all cursors.
758	fn cursor_max_encoded_len() -> usize;
759
760	/// The maximal encoded length across all identifiers.
761	fn identifier_max_encoded_len() -> usize;
762
763	/// Assert the integrity of the migrations.
764	///
765	/// Should be executed as part of a test prior to runtime usage. May or may not need
766	/// externalities.
767	#[cfg(feature = "std")]
768	fn integrity_test() -> Result<(), &'static str> {
769		use crate::ensure;
770		let l = Self::len();
771
772		for n in 0..l {
773			ensure!(Self::nth_id(n).is_some(), "id is None");
774			ensure!(Self::nth_max_steps(n).is_some(), "steps is None");
775
776			// The cursor that we use does not matter. Hence use empty.
777			ensure!(
778				Self::nth_step(n, Some(vec![]), &mut WeightMeter::new()).is_some(),
779				"steps is None"
780			);
781			ensure!(
782				Self::nth_transactional_step(n, Some(vec![]), &mut WeightMeter::new()).is_some(),
783				"steps is None"
784			);
785		}
786
787		Ok(())
788	}
789}
790
791impl SteppedMigrations for () {
792	fn len() -> u32 {
793		0
794	}
795
796	fn nth_id(_n: u32) -> Option<Vec<u8>> {
797		None
798	}
799
800	fn nth_max_steps(_n: u32) -> Option<Option<u32>> {
801		None
802	}
803
804	fn nth_step(
805		_n: u32,
806		_cursor: Option<Vec<u8>>,
807		_meter: &mut WeightMeter,
808	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
809		None
810	}
811
812	fn nth_transactional_step(
813		_n: u32,
814		_cursor: Option<Vec<u8>>,
815		_meter: &mut WeightMeter,
816	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
817		None
818	}
819
820	fn nth_migrating_prefixes(_n: u32) -> Option<Option<Vec<Vec<u8>>>> {
821		None
822	}
823
824	#[cfg(feature = "try-runtime")]
825	fn nth_pre_upgrade(_n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
826		Some(Ok(Vec::new()))
827	}
828
829	#[cfg(feature = "try-runtime")]
830	fn nth_post_upgrade(
831		_n: u32,
832		_state: Vec<u8>,
833	) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
834		Some(Ok(()))
835	}
836
837	fn cursor_max_encoded_len() -> usize {
838		0
839	}
840
841	fn identifier_max_encoded_len() -> usize {
842		0
843	}
844}
845
846// A collection consisting of only a single migration.
847impl<T: SteppedMigration> SteppedMigrations for T {
848	fn len() -> u32 {
849		1
850	}
851	// It should be generally fine to call with n>0, but the code should not attempt to.
852	fn nth_id(n: u32) -> Option<Vec<u8>> {
853		n.is_zero()
854			.then(|| T::id().encode())
855			.defensive_proof("nth_id should only be called with n==0")
856	}
857
858	fn nth_max_steps(n: u32) -> Option<Option<u32>> {
859		n.is_zero()
860			.then(|| T::max_steps())
861			.defensive_proof("nth_max_steps should only be called with n==0")
862	}
863
864	fn nth_step(
865		n: u32,
866		cursor: Option<Vec<u8>>,
867		meter: &mut WeightMeter,
868	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
869		if !n.is_zero() {
870			defensive!("nth_step should only be called with n==0");
871			return None;
872		}
873
874		let cursor = match cursor {
875			Some(cursor) => match T::Cursor::decode(&mut &cursor[..]) {
876				Ok(cursor) => Some(cursor),
877				Err(_) => return Some(Err(SteppedMigrationError::InvalidCursor)),
878			},
879			None => None,
880		};
881
882		Some(T::step(cursor, meter).map(|cursor| cursor.map(|cursor| cursor.encode())))
883	}
884
885	fn nth_transactional_step(
886		n: u32,
887		cursor: Option<Vec<u8>>,
888		meter: &mut WeightMeter,
889	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
890		if n != 0 {
891			defensive!("nth_transactional_step should only be called with n==0");
892			return None;
893		}
894
895		let cursor = match cursor {
896			Some(cursor) => match T::Cursor::decode(&mut &cursor[..]) {
897				Ok(cursor) => Some(cursor),
898				Err(_) => return Some(Err(SteppedMigrationError::InvalidCursor)),
899			},
900			None => None,
901		};
902
903		Some(
904			T::transactional_step(cursor, meter).map(|cursor| cursor.map(|cursor| cursor.encode())),
905		)
906	}
907
908	fn nth_migrating_prefixes(n: u32) -> Option<Option<Vec<Vec<u8>>>> {
909		n.is_zero()
910			.then(|| T::migrating_prefixes().map(|p| p.into_iter().collect()))
911			.defensive_proof("nth_migrating_prefixes should only be called with n==0")
912	}
913
914	#[cfg(feature = "try-runtime")]
915	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
916		if n != 0 {
917			defensive!("nth_pre_upgrade should only be called with n==0");
918		}
919
920		Some(T::pre_upgrade())
921	}
922
923	#[cfg(feature = "try-runtime")]
924	fn nth_post_upgrade(n: u32, state: Vec<u8>) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
925		if n != 0 {
926			defensive!("nth_post_upgrade should only be called with n==0");
927		}
928		Some(T::post_upgrade(state))
929	}
930
931	fn cursor_max_encoded_len() -> usize {
932		T::Cursor::max_encoded_len()
933	}
934
935	fn identifier_max_encoded_len() -> usize {
936		T::Identifier::max_encoded_len()
937	}
938}
939
940#[impl_trait_for_tuples::impl_for_tuples(1, 30)]
941impl SteppedMigrations for Tuple {
942	fn len() -> u32 {
943		for_tuples!( #( Tuple::len() )+* )
944	}
945
946	fn nth_id(n: u32) -> Option<Vec<u8>> {
947		let mut i = 0;
948
949		for_tuples!( #(
950			if (i + Tuple::len()) > n {
951				return Tuple::nth_id(n - i)
952			}
953
954			i += Tuple::len();
955		)* );
956
957		None
958	}
959
960	fn nth_step(
961		n: u32,
962		cursor: Option<Vec<u8>>,
963		meter: &mut WeightMeter,
964	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
965		let mut i = 0;
966
967		for_tuples!( #(
968			if (i + Tuple::len()) > n {
969				return Tuple::nth_step(n - i, cursor, meter)
970			}
971
972			i += Tuple::len();
973		)* );
974
975		None
976	}
977
978	fn nth_transactional_step(
979		n: u32,
980		cursor: Option<Vec<u8>>,
981		meter: &mut WeightMeter,
982	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
983		let mut i = 0;
984
985		for_tuples! ( #(
986			if (i + Tuple::len()) > n {
987				return Tuple::nth_transactional_step(n - i, cursor, meter)
988			}
989
990			i += Tuple::len();
991		)* );
992
993		None
994	}
995
996	fn nth_migrating_prefixes(n: u32) -> Option<Option<Vec<Vec<u8>>>> {
997		let mut i = 0;
998		for_tuples!( #(
999            let len = Tuple::len() as u32;
1000            if (i + len) > n {
1001                return Tuple::nth_migrating_prefixes(n - i);
1002            }
1003            i += len;
1004        )* );
1005		None
1006	}
1007
1008	#[cfg(feature = "try-runtime")]
1009	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
1010		let mut i = 0;
1011
1012		for_tuples! ( #(
1013			if (i + Tuple::len()) > n {
1014				return Tuple::nth_pre_upgrade(n - i)
1015			}
1016
1017			i += Tuple::len();
1018		)* );
1019
1020		None
1021	}
1022
1023	#[cfg(feature = "try-runtime")]
1024	fn nth_post_upgrade(n: u32, state: Vec<u8>) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
1025		let mut i = 0;
1026
1027		for_tuples! ( #(
1028			if (i + Tuple::len()) > n {
1029				return Tuple::nth_post_upgrade(n - i, state)
1030			}
1031
1032			i += Tuple::len();
1033		)* );
1034
1035		None
1036	}
1037
1038	fn nth_max_steps(n: u32) -> Option<Option<u32>> {
1039		let mut i = 0;
1040
1041		for_tuples!( #(
1042			if (i + Tuple::len()) > n {
1043				return Tuple::nth_max_steps(n - i)
1044			}
1045
1046			i += Tuple::len();
1047		)* );
1048
1049		None
1050	}
1051
1052	fn cursor_max_encoded_len() -> usize {
1053		let mut max_len = 0;
1054
1055		for_tuples!( #(
1056			max_len = max_len.max(Tuple::cursor_max_encoded_len());
1057		)* );
1058
1059		max_len
1060	}
1061
1062	fn identifier_max_encoded_len() -> usize {
1063		let mut max_len = 0;
1064
1065		for_tuples!( #(
1066			max_len = max_len.max(Tuple::identifier_max_encoded_len());
1067		)* );
1068
1069		max_len
1070	}
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075	use super::*;
1076	use crate::{assert_ok, storage::unhashed};
1077
1078	#[derive(Decode, Encode, MaxEncodedLen, Eq, PartialEq)]
1079	#[allow(dead_code)]
1080	pub enum Either<L, R> {
1081		Left(L),
1082		Right(R),
1083	}
1084
1085	pub struct M0;
1086	impl SteppedMigration for M0 {
1087		type Cursor = ();
1088		type Identifier = u8;
1089
1090		fn id() -> Self::Identifier {
1091			0
1092		}
1093
1094		fn step(
1095			_cursor: Option<Self::Cursor>,
1096			_meter: &mut WeightMeter,
1097		) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
1098			log::info!("M0");
1099			unhashed::put(&[0], &());
1100			Ok(None)
1101		}
1102
1103		fn migrating_prefixes() -> Option<impl IntoIterator<Item = Vec<u8>>> {
1104			Some(vec![b"M0_prefix1".to_vec(), b"M0_prefix2".to_vec()])
1105		}
1106	}
1107
1108	pub struct M1;
1109	impl SteppedMigration for M1 {
1110		type Cursor = ();
1111		type Identifier = u8;
1112
1113		fn id() -> Self::Identifier {
1114			1
1115		}
1116
1117		fn step(
1118			_cursor: Option<Self::Cursor>,
1119			_meter: &mut WeightMeter,
1120		) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
1121			log::info!("M1");
1122			unhashed::put(&[1], &());
1123			Ok(None)
1124		}
1125
1126		fn max_steps() -> Option<u32> {
1127			Some(1)
1128		}
1129
1130		fn migrating_prefixes() -> Option<impl IntoIterator<Item = Vec<u8>>> {
1131			Some(vec![b"M1_prefix".to_vec()])
1132		}
1133	}
1134
1135	pub struct M2;
1136	impl SteppedMigration for M2 {
1137		type Cursor = ();
1138		type Identifier = u8;
1139
1140		fn id() -> Self::Identifier {
1141			2
1142		}
1143
1144		fn step(
1145			_cursor: Option<Self::Cursor>,
1146			_meter: &mut WeightMeter,
1147		) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
1148			log::info!("M2");
1149			unhashed::put(&[2], &());
1150			Ok(None)
1151		}
1152
1153		fn max_steps() -> Option<u32> {
1154			Some(2)
1155		}
1156
1157		fn migrating_prefixes() -> Option<impl IntoIterator<Item = Vec<u8>>> {
1158			Some(vec![b"M2_prefix1".to_vec(), b"M2_prefix2".to_vec(), b"M2_prefix3".to_vec()])
1159		}
1160	}
1161
1162	pub struct M3;
1163	impl SteppedMigration for M3 {
1164		type Cursor = ();
1165		type Identifier = u8;
1166
1167		fn id() -> Self::Identifier {
1168			3
1169		}
1170
1171		fn step(
1172			_cursor: Option<Self::Cursor>,
1173			_meter: &mut WeightMeter,
1174		) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
1175			log::info!("M3");
1176			Ok(None)
1177		}
1178	}
1179
1180	pub struct F0;
1181	impl SteppedMigration for F0 {
1182		type Cursor = ();
1183		type Identifier = u8;
1184
1185		fn id() -> Self::Identifier {
1186			4
1187		}
1188
1189		fn step(
1190			_cursor: Option<Self::Cursor>,
1191			_meter: &mut WeightMeter,
1192		) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
1193			log::info!("F0");
1194			unhashed::put(&[3], &());
1195			Err(SteppedMigrationError::Failed)
1196		}
1197	}
1198
1199	// Three migrations combined to execute in order:
1200	type Triple = (M0, (M1, M2));
1201	// Six migrations, just concatenating the ones from before:
1202	type Hextuple = (Triple, Triple);
1203
1204	#[test]
1205	fn singular_migrations_work() {
1206		assert_eq!(M0::max_steps(), None);
1207		assert_eq!(M1::max_steps(), Some(1));
1208		assert_eq!(M2::max_steps(), Some(2));
1209
1210		assert_eq!(<(M0, M1)>::nth_max_steps(0), Some(None));
1211		assert_eq!(<(M0, M1)>::nth_max_steps(1), Some(Some(1)));
1212		assert_eq!(<(M0, M1, M2)>::nth_max_steps(2), Some(Some(2)));
1213
1214		assert_eq!(<(M0, M1)>::nth_max_steps(2), None);
1215	}
1216
1217	#[test]
1218	fn tuple_migrations_work() {
1219		assert_eq!(<() as SteppedMigrations>::len(), 0);
1220		assert_eq!(<((), ((), ())) as SteppedMigrations>::len(), 0);
1221		assert_eq!(<Triple as SteppedMigrations>::len(), 3);
1222		assert_eq!(<Hextuple as SteppedMigrations>::len(), 6);
1223
1224		// Check the IDs. The index specific functions all return an Option,
1225		// to account for the out-of-range case.
1226		assert_eq!(<Triple as SteppedMigrations>::nth_id(0), Some(0u8.encode()));
1227		assert_eq!(<Triple as SteppedMigrations>::nth_id(1), Some(1u8.encode()));
1228		assert_eq!(<Triple as SteppedMigrations>::nth_id(2), Some(2u8.encode()));
1229
1230		sp_io::TestExternalities::default().execute_with(|| {
1231			for n in 0..3 {
1232				<Triple as SteppedMigrations>::nth_step(
1233					n,
1234					Default::default(),
1235					&mut WeightMeter::new(),
1236				);
1237			}
1238		});
1239	}
1240
1241	#[test]
1242	fn integrity_test_works() {
1243		sp_io::TestExternalities::default().execute_with(|| {
1244			assert_ok!(<() as SteppedMigrations>::integrity_test());
1245			assert_ok!(<M0 as SteppedMigrations>::integrity_test());
1246			assert_ok!(<M1 as SteppedMigrations>::integrity_test());
1247			assert_ok!(<M2 as SteppedMigrations>::integrity_test());
1248			assert_ok!(<Triple as SteppedMigrations>::integrity_test());
1249			assert_ok!(<Hextuple as SteppedMigrations>::integrity_test());
1250		});
1251	}
1252
1253	#[test]
1254	fn transactional_rollback_works() {
1255		sp_io::TestExternalities::default().execute_with(|| {
1256			assert_ok!(<(M0, F0) as SteppedMigrations>::nth_transactional_step(
1257				0,
1258				Default::default(),
1259				&mut WeightMeter::new()
1260			)
1261			.unwrap());
1262			assert!(unhashed::exists(&[0]));
1263
1264			let _g = crate::StorageNoopGuard::new();
1265			assert!(<(M0, F0) as SteppedMigrations>::nth_transactional_step(
1266				1,
1267				Default::default(),
1268				&mut WeightMeter::new()
1269			)
1270			.unwrap()
1271			.is_err());
1272			assert!(<(F0, M1) as SteppedMigrations>::nth_transactional_step(
1273				0,
1274				Default::default(),
1275				&mut WeightMeter::new()
1276			)
1277			.unwrap()
1278			.is_err());
1279		});
1280	}
1281
1282	#[test]
1283	fn nth_migrating_prefixes_works() {
1284		// Test single migration
1285		assert_eq!(
1286			M0::nth_migrating_prefixes(0),
1287			Some(Some(vec![b"M0_prefix1".to_vec(), b"M0_prefix2".to_vec()]))
1288		);
1289
1290		// Test migration with no prefixes
1291		assert_eq!(M3::nth_migrating_prefixes(0), Some(None));
1292
1293		// Test tuple migrations
1294		type Pair = (M0, M1);
1295		assert_eq!(Pair::len(), 2);
1296
1297		// First migration in tuple
1298		assert_eq!(
1299			Pair::nth_migrating_prefixes(0),
1300			Some(Some(vec![b"M0_prefix1".to_vec(), b"M0_prefix2".to_vec()]))
1301		);
1302
1303		// Second migration in tuple
1304		assert_eq!(Pair::nth_migrating_prefixes(1), Some(Some(vec![b"M1_prefix".to_vec()])));
1305
1306		// Out of bounds
1307		assert_eq!(Pair::nth_migrating_prefixes(2), None);
1308
1309		// Test nested tuples
1310		type Nested = (M0, (M1, M2));
1311		assert_eq!(Nested::len(), 3);
1312
1313		// First migration
1314		assert_eq!(
1315			Nested::nth_migrating_prefixes(0),
1316			Some(Some(vec![b"M0_prefix1".to_vec(), b"M0_prefix2".to_vec()]))
1317		);
1318
1319		// Second migration (first in inner tuple)
1320		assert_eq!(Nested::nth_migrating_prefixes(1), Some(Some(vec![b"M1_prefix".to_vec()])));
1321
1322		// Third migration (second in inner tuple)
1323		assert_eq!(
1324			Nested::nth_migrating_prefixes(2),
1325			Some(Some(vec![
1326				b"M2_prefix1".to_vec(),
1327				b"M2_prefix2".to_vec(),
1328				b"M2_prefix3".to_vec()
1329			]))
1330		);
1331
1332		assert_eq!(Nested::nth_migrating_prefixes(3), None);
1333	}
1334}