Skip to main content

topsoil_core/
migrations.rs

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