referrerpolicy=no-referrer-when-downgrade

pallet_xcm/
migration.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot 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// Polkadot 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 Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17use crate::{
18	pallet::CurrentMigration, Config, CurrentXcmVersion, Pallet, VersionMigrationStage,
19	VersionNotifyTargets,
20};
21use frame_support::{
22	pallet_prelude::*,
23	traits::{OnRuntimeUpgrade, StorageVersion, UncheckedOnRuntimeUpgrade},
24	weights::Weight,
25};
26
27const DEFAULT_PROOF_SIZE: u64 = 64 * 1024;
28
29/// Utilities for handling XCM version migration for the relevant data.
30pub mod data {
31	use crate::*;
32
33	/// A trait for handling XCM versioned data migration for the requested `XcmVersion`.
34	pub(crate) trait NeedsMigration {
35		type MigratedData;
36
37		/// Returns true if data does not match `minimal_allowed_xcm_version`.
38		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool;
39
40		/// Attempts to migrate data. `Ok(None)` means no migration is needed.
41		/// `Ok(Some(Self::MigratedData))` should contain the migrated data.
42		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()>;
43	}
44
45	/// Implementation of `NeedsMigration` for `LockedFungibles` data.
46	impl<B, M> NeedsMigration for BoundedVec<(B, VersionedLocation), M> {
47		type MigratedData = Self;
48
49		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
50			self.iter()
51				.any(|(_, unlocker)| unlocker.identify_version() < minimal_allowed_xcm_version)
52		}
53
54		fn try_migrate(
55			mut self,
56			to_xcm_version: XcmVersion,
57		) -> Result<Option<Self::MigratedData>, ()> {
58			let mut was_modified = false;
59			for locked in self.iter_mut() {
60				if locked.1.identify_version() < to_xcm_version {
61					let Ok(new_unlocker) = locked.1.clone().into_version(to_xcm_version) else {
62						return Err(())
63					};
64					locked.1 = new_unlocker;
65					was_modified = true;
66				}
67			}
68
69			if was_modified {
70				Ok(Some(self))
71			} else {
72				Ok(None)
73			}
74		}
75	}
76
77	/// Implementation of `NeedsMigration` for `Queries` data.
78	impl<BlockNumber> NeedsMigration for QueryStatus<BlockNumber> {
79		type MigratedData = Self;
80
81		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
82			match &self {
83				QueryStatus::Pending { responder, maybe_match_querier, .. } =>
84					responder.identify_version() < minimal_allowed_xcm_version ||
85						maybe_match_querier
86							.as_ref()
87							.map(|v| v.identify_version() < minimal_allowed_xcm_version)
88							.unwrap_or(false),
89				QueryStatus::VersionNotifier { origin, .. } =>
90					origin.identify_version() < minimal_allowed_xcm_version,
91				QueryStatus::Ready { response, .. } =>
92					response.identify_version() < minimal_allowed_xcm_version,
93			}
94		}
95
96		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
97			if !self.needs_migration(to_xcm_version) {
98				return Ok(None)
99			}
100
101			// do migration
102			match self {
103				QueryStatus::Pending { responder, maybe_match_querier, maybe_notify, timeout } => {
104					let Ok(responder) = responder.into_version(to_xcm_version) else {
105						return Err(())
106					};
107					let Ok(maybe_match_querier) =
108						maybe_match_querier.map(|mmq| mmq.into_version(to_xcm_version)).transpose()
109					else {
110						return Err(())
111					};
112					Ok(Some(QueryStatus::Pending {
113						responder,
114						maybe_match_querier,
115						maybe_notify,
116						timeout,
117					}))
118				},
119				QueryStatus::VersionNotifier { origin, is_active } => origin
120					.into_version(to_xcm_version)
121					.map(|origin| Some(QueryStatus::VersionNotifier { origin, is_active })),
122				QueryStatus::Ready { response, at } => response
123					.into_version(to_xcm_version)
124					.map(|response| Some(QueryStatus::Ready { response, at })),
125			}
126		}
127	}
128
129	/// Implementation of `NeedsMigration` for `RemoteLockedFungibles` key type.
130	impl<A> NeedsMigration for (XcmVersion, A, VersionedAssetId) {
131		type MigratedData = Self;
132
133		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
134			self.0 < minimal_allowed_xcm_version ||
135				self.2.identify_version() < minimal_allowed_xcm_version
136		}
137
138		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
139			if !self.needs_migration(to_xcm_version) {
140				return Ok(None)
141			}
142
143			let Ok(asset_id) = self.2.into_version(to_xcm_version) else { return Err(()) };
144			Ok(Some((to_xcm_version, self.1, asset_id)))
145		}
146	}
147
148	/// Implementation of `NeedsMigration` for `RemoteLockedFungibles` data.
149	impl<ConsumerIdentifier, MaxConsumers: Get<u32>> NeedsMigration
150		for RemoteLockedFungibleRecord<ConsumerIdentifier, MaxConsumers>
151	{
152		type MigratedData = Self;
153
154		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
155			self.owner.identify_version() < minimal_allowed_xcm_version ||
156				self.locker.identify_version() < minimal_allowed_xcm_version
157		}
158
159		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
160			if !self.needs_migration(to_xcm_version) {
161				return Ok(None)
162			}
163
164			let RemoteLockedFungibleRecord { amount, owner, locker, consumers } = self;
165
166			let Ok(owner) = owner.into_version(to_xcm_version) else { return Err(()) };
167			let Ok(locker) = locker.into_version(to_xcm_version) else { return Err(()) };
168
169			Ok(Some(RemoteLockedFungibleRecord { amount, owner, locker, consumers }))
170		}
171	}
172
173	/// Implementation of `NeedsMigration` for `AuthorizedAliases` data.
174	impl<M: Get<u32>, T: Config> NeedsMigration
175		for (&VersionedLocation, AuthorizedAliasesEntry<TicketOf<T>, M>, PhantomData<T>)
176	{
177		type MigratedData = (VersionedLocation, AuthorizedAliasesEntry<TicketOf<T>, M>);
178
179		fn needs_migration(&self, required_version: XcmVersion) -> bool {
180			self.0.identify_version() != required_version ||
181				self.1
182					.aliasers
183					.iter()
184					.any(|alias| alias.location.identify_version() != required_version)
185		}
186
187		fn try_migrate(
188			self,
189			required_version: XcmVersion,
190		) -> Result<Option<Self::MigratedData>, ()> {
191			if !self.needs_migration(required_version) {
192				return Ok(None)
193			}
194
195			let key = if self.0.identify_version() != required_version {
196				let Ok(converted_key) = self.0.clone().into_version(required_version) else {
197					return Err(())
198				};
199				converted_key
200			} else {
201				self.0.clone()
202			};
203
204			let mut new_aliases = BoundedVec::<OriginAliaser, M>::new();
205			let (aliasers, ticket) = (self.1.aliasers, self.1.ticket);
206			for alias in aliasers {
207				let OriginAliaser { mut location, expiry } = alias.clone();
208				if location.identify_version() != required_version {
209					location = location.into_version(required_version)?;
210				}
211				new_aliases.try_push(OriginAliaser { location, expiry }).map_err(|_| ())?;
212			}
213
214			Ok(Some((key, AuthorizedAliasesEntry { aliasers: new_aliases, ticket })))
215		}
216	}
217
218	impl<T: Config> Pallet<T> {
219		/// Migrates relevant data to the `required_xcm_version`.
220		pub(crate) fn migrate_data_to_xcm_version(
221			weight: &mut Weight,
222			required_xcm_version: XcmVersion,
223		) {
224			const LOG_TARGET: &str = "runtime::xcm::pallet_xcm::migrate_data_to_xcm_version";
225
226			// check and migrate `Queries`
227			let queries_to_migrate = Queries::<T>::iter().filter_map(|(id, data)| {
228				weight.saturating_add(T::DbWeight::get().reads(1));
229				match data.try_migrate(required_xcm_version) {
230					Ok(Some(new_data)) => Some((id, new_data)),
231					Ok(None) => None,
232					Err(_) => {
233						tracing::error!(
234							target: LOG_TARGET,
235							?id,
236							?required_xcm_version,
237							"`Queries` cannot be migrated!"
238						);
239						None
240					},
241				}
242			});
243			for (id, new_data) in queries_to_migrate {
244				tracing::info!(
245					target: LOG_TARGET,
246					query_id = ?id,
247					?new_data,
248					"Migrating `Queries`"
249				);
250				Queries::<T>::insert(id, new_data);
251				weight.saturating_add(T::DbWeight::get().writes(1));
252			}
253
254			// check and migrate `LockedFungibles`
255			let locked_fungibles_to_migrate =
256				LockedFungibles::<T>::iter().filter_map(|(id, data)| {
257					weight.saturating_add(T::DbWeight::get().reads(1));
258					match data.try_migrate(required_xcm_version) {
259						Ok(Some(new_data)) => Some((id, new_data)),
260						Ok(None) => None,
261						Err(_) => {
262							tracing::error!(
263								target: LOG_TARGET,
264								?id,
265								?required_xcm_version,
266								"`LockedFungibles` cannot be migrated!"
267							);
268							None
269						},
270					}
271				});
272			for (id, new_data) in locked_fungibles_to_migrate {
273				tracing::info!(
274					target: LOG_TARGET,
275					account_id = ?id,
276					?new_data,
277					"Migrating `LockedFungibles`"
278				);
279				LockedFungibles::<T>::insert(id, new_data);
280				weight.saturating_add(T::DbWeight::get().writes(1));
281			}
282
283			// check and migrate `RemoteLockedFungibles` - 1. step - just data
284			let remote_locked_fungibles_to_migrate =
285				RemoteLockedFungibles::<T>::iter().filter_map(|(id, data)| {
286					weight.saturating_add(T::DbWeight::get().reads(1));
287					match data.try_migrate(required_xcm_version) {
288						Ok(Some(new_data)) => Some((id, new_data)),
289						Ok(None) => None,
290						Err(_) => {
291							tracing::error!(
292								target: LOG_TARGET,
293								?id,
294								?required_xcm_version,
295								"`RemoteLockedFungibles` data cannot be migrated!"
296							);
297							None
298						},
299					}
300				});
301			for (id, new_data) in remote_locked_fungibles_to_migrate {
302				tracing::info!(
303					target: LOG_TARGET,
304					key = ?id,
305					amount = ?new_data.amount,
306					locker = ?new_data.locker,
307					owner = ?new_data.owner,
308					consumers_count = ?new_data.consumers.len(),
309					"Migrating `RemoteLockedFungibles` data"
310				);
311				RemoteLockedFungibles::<T>::insert(id, new_data);
312				weight.saturating_add(T::DbWeight::get().writes(1));
313			}
314
315			// check and migrate `RemoteLockedFungibles` - 2. step - key
316			let remote_locked_fungibles_keys_to_migrate = RemoteLockedFungibles::<T>::iter_keys()
317				.filter_map(|key| {
318					if key.needs_migration(required_xcm_version) {
319						let old_key = key.clone();
320						match key.try_migrate(required_xcm_version) {
321							Ok(Some(new_key)) => Some((old_key, new_key)),
322							Ok(None) => None,
323							Err(_) => {
324								tracing::error!(
325									target: LOG_TARGET,
326									id = ?old_key,
327									?required_xcm_version,
328									"`RemoteLockedFungibles` key cannot be migrated!"
329								);
330								None
331							},
332						}
333					} else {
334						None
335					}
336				});
337			for (old_key, new_key) in remote_locked_fungibles_keys_to_migrate {
338				weight.saturating_add(T::DbWeight::get().reads(1));
339				// make sure, that we don't override accidentally other data
340				if RemoteLockedFungibles::<T>::get(&new_key).is_some() {
341					tracing::error!(
342						target: LOG_TARGET,
343						?old_key,
344						?new_key,
345						"`RemoteLockedFungibles` already contains data for a `new_key`!"
346					);
347					// let's just skip for now, could be potentially caused with missing this
348					// migration before (manual clean-up?).
349					continue;
350				}
351
352				tracing::info!(
353					target: LOG_TARGET,
354					?old_key,
355					?new_key,
356					"Migrating `RemoteLockedFungibles` key"
357				);
358
359				// now we can swap the keys
360				RemoteLockedFungibles::<T>::swap::<
361					(
362						NMapKey<Twox64Concat, XcmVersion>,
363						NMapKey<Blake2_128Concat, T::AccountId>,
364						NMapKey<Blake2_128Concat, VersionedAssetId>,
365					),
366					_,
367					_,
368				>(&old_key, &new_key);
369				weight.saturating_add(T::DbWeight::get().writes(1));
370			}
371
372			// check and migrate `AuthorizedAliases`
373			let aliases_to_migrate = AuthorizedAliases::<T>::iter().filter_map(|(id, data)| {
374				weight.saturating_add(T::DbWeight::get().reads(1));
375				match (&id, data, PhantomData::<T>).try_migrate(required_xcm_version) {
376					Ok(Some((new_id, new_data))) => Some((id, new_id, new_data)),
377					Ok(None) => None,
378					Err(_) => {
379						tracing::error!(
380							target: LOG_TARGET,
381							?id,
382							?required_xcm_version,
383							"`AuthorizedAliases` cannot be migrated!"
384						);
385						None
386					},
387				}
388			});
389			let mut count = 0;
390			for (old_id, new_id, new_data) in aliases_to_migrate {
391				tracing::info!(
392					target: LOG_TARGET,
393					?new_id,
394					?new_data,
395					"Migrating `AuthorizedAliases`"
396				);
397				AuthorizedAliases::<T>::remove(old_id);
398				AuthorizedAliases::<T>::insert(new_id, new_data);
399				count = count + 1;
400			}
401			// two writes per key, one to remove old entry, one to write new entry
402			weight.saturating_add(T::DbWeight::get().writes(count * 2));
403		}
404	}
405}
406
407pub mod v1 {
408	use super::*;
409	use crate::{CurrentMigration, VersionMigrationStage};
410
411	/// Named with the 'VersionUnchecked'-prefix because although this implements some version
412	/// checking, the version checking is not complete as it will begin failing after the upgrade is
413	/// enacted on-chain.
414	///
415	/// Use experimental [`MigrateToV1`] instead.
416	pub struct VersionUncheckedMigrateToV1<T>(core::marker::PhantomData<T>);
417	impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateToV1<T> {
418		fn on_runtime_upgrade() -> Weight {
419			let mut weight = T::DbWeight::get().reads(1);
420
421			if StorageVersion::get::<Pallet<T>>() != 0 {
422				tracing::warn!("skipping v1, should be removed");
423				return weight
424			}
425
426			weight.saturating_accrue(T::DbWeight::get().writes(1));
427			CurrentMigration::<T>::put(VersionMigrationStage::default());
428
429			let translate = |pre: (u64, u64, u32)| -> Option<(u64, Weight, u32)> {
430				weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
431				let translated = (pre.0, Weight::from_parts(pre.1, DEFAULT_PROOF_SIZE), pre.2);
432				tracing::info!("Migrated VersionNotifyTarget {:?} to {:?}", pre, translated);
433				Some(translated)
434			};
435
436			VersionNotifyTargets::<T>::translate_values(translate);
437
438			tracing::info!("v1 applied successfully");
439			weight.saturating_accrue(T::DbWeight::get().writes(1));
440			StorageVersion::new(1).put::<Pallet<T>>();
441			weight
442		}
443	}
444
445	/// Version checked migration to v1.
446	///
447	/// Wrapped in [`frame_support::migrations::VersionedMigration`] so the pre/post checks don't
448	/// begin failing after the upgrade is enacted on-chain.
449	pub type MigrateToV1<T> = frame_support::migrations::VersionedMigration<
450		0,
451		1,
452		VersionUncheckedMigrateToV1<T>,
453		crate::pallet::Pallet<T>,
454		<T as frame_system::Config>::DbWeight,
455	>;
456}
457
458/// When adding a new XCM version, we need to run this migration for `pallet_xcm` to ensure that all
459/// previously stored data with subkey prefix `XCM_VERSION-1` (and below) are migrated to the
460/// `XCM_VERSION`.
461///
462/// NOTE: This migration can be permanently added to the runtime migrations.
463pub struct MigrateToLatestXcmVersion<T>(core::marker::PhantomData<T>);
464impl<T: Config> OnRuntimeUpgrade for MigrateToLatestXcmVersion<T> {
465	fn on_runtime_upgrade() -> Weight {
466		let mut weight = T::DbWeight::get().reads(1);
467
468		// trigger expensive/lazy migration (kind of multi-block)
469		CurrentMigration::<T>::put(VersionMigrationStage::default());
470		weight.saturating_accrue(T::DbWeight::get().writes(1));
471
472		// migrate other operational data to the latest XCM version in-place
473		let latest = CurrentXcmVersion::get();
474		Pallet::<T>::migrate_data_to_xcm_version(&mut weight, latest);
475
476		weight
477	}
478
479	#[cfg(feature = "try-runtime")]
480	fn post_upgrade(_: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
481		use data::NeedsMigration;
482		const LOG_TARGET: &str = "runtime::xcm::pallet_xcm::migrate_to_latest";
483
484		let latest = CurrentXcmVersion::get();
485
486		let number_of_queries_to_migrate = crate::Queries::<T>::iter()
487			.filter(|(id, data)| {
488				let needs_migration = data.needs_migration(latest);
489				if needs_migration {
490					tracing::warn!(
491						target: LOG_TARGET,
492						query_id = ?id,
493						query = ?data,
494						"Query was not migrated!"
495					)
496				}
497				needs_migration
498			})
499			.count();
500
501		let number_of_locked_fungibles_to_migrate = crate::LockedFungibles::<T>::iter()
502			.filter_map(|(id, data)| {
503				if data.needs_migration(latest) {
504					tracing::warn!(
505						target: LOG_TARGET,
506						?id,
507						?data,
508						"LockedFungibles item was not migrated!"
509					);
510					Some(true)
511				} else {
512					None
513				}
514			})
515			.count();
516
517		let number_of_remote_locked_fungibles_to_migrate =
518			crate::RemoteLockedFungibles::<T>::iter()
519				.filter_map(|(key, data)| {
520					if key.needs_migration(latest) || data.needs_migration(latest) {
521						tracing::warn!(
522							target: LOG_TARGET,
523							?key,
524							"RemoteLockedFungibles item was not migrated!"
525						);
526						Some(true)
527					} else {
528						None
529					}
530				})
531				.count();
532
533		ensure!(number_of_queries_to_migrate == 0, "must migrate all `Queries`.");
534		ensure!(number_of_locked_fungibles_to_migrate == 0, "must migrate all `LockedFungibles`.");
535		ensure!(
536			number_of_remote_locked_fungibles_to_migrate == 0,
537			"must migrate all `RemoteLockedFungibles`."
538		);
539
540		Ok(())
541	}
542}