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				},
90				QueryStatus::VersionNotifier { origin, .. } => {
91					origin.identify_version() < minimal_allowed_xcm_version
92				},
93				QueryStatus::Ready { response, .. } => {
94					response.identify_version() < minimal_allowed_xcm_version
95				},
96			}
97		}
98
99		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
100			if !self.needs_migration(to_xcm_version) {
101				return Ok(None);
102			}
103
104			// do migration
105			match self {
106				QueryStatus::Pending { responder, maybe_match_querier, maybe_notify, timeout } => {
107					let Ok(responder) = responder.into_version(to_xcm_version) else {
108						return Err(());
109					};
110					let Ok(maybe_match_querier) =
111						maybe_match_querier.map(|mmq| mmq.into_version(to_xcm_version)).transpose()
112					else {
113						return Err(());
114					};
115					Ok(Some(QueryStatus::Pending {
116						responder,
117						maybe_match_querier,
118						maybe_notify,
119						timeout,
120					}))
121				},
122				QueryStatus::VersionNotifier { origin, is_active } => origin
123					.into_version(to_xcm_version)
124					.map(|origin| Some(QueryStatus::VersionNotifier { origin, is_active })),
125				QueryStatus::Ready { response, at } => response
126					.into_version(to_xcm_version)
127					.map(|response| Some(QueryStatus::Ready { response, at })),
128			}
129		}
130	}
131
132	/// Implementation of `NeedsMigration` for `RemoteLockedFungibles` key type.
133	impl<A> NeedsMigration for (XcmVersion, A, VersionedAssetId) {
134		type MigratedData = Self;
135
136		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
137			self.0 < minimal_allowed_xcm_version ||
138				self.2.identify_version() < minimal_allowed_xcm_version
139		}
140
141		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
142			if !self.needs_migration(to_xcm_version) {
143				return Ok(None);
144			}
145
146			let Ok(asset_id) = self.2.into_version(to_xcm_version) else { return Err(()) };
147			Ok(Some((to_xcm_version, self.1, asset_id)))
148		}
149	}
150
151	/// Implementation of `NeedsMigration` for `RemoteLockedFungibles` data.
152	impl<ConsumerIdentifier, MaxConsumers: Get<u32>> NeedsMigration
153		for RemoteLockedFungibleRecord<ConsumerIdentifier, MaxConsumers>
154	{
155		type MigratedData = Self;
156
157		fn needs_migration(&self, minimal_allowed_xcm_version: XcmVersion) -> bool {
158			self.owner.identify_version() < minimal_allowed_xcm_version ||
159				self.locker.identify_version() < minimal_allowed_xcm_version
160		}
161
162		fn try_migrate(self, to_xcm_version: XcmVersion) -> Result<Option<Self::MigratedData>, ()> {
163			if !self.needs_migration(to_xcm_version) {
164				return Ok(None);
165			}
166
167			let RemoteLockedFungibleRecord { amount, owner, locker, consumers } = self;
168
169			let Ok(owner) = owner.into_version(to_xcm_version) else { return Err(()) };
170			let Ok(locker) = locker.into_version(to_xcm_version) else { return Err(()) };
171
172			Ok(Some(RemoteLockedFungibleRecord { amount, owner, locker, consumers }))
173		}
174	}
175
176	/// Implementation of `NeedsMigration` for `AuthorizedAliases` data.
177	impl<M: Get<u32>, T: Config> NeedsMigration
178		for (&VersionedLocation, AuthorizedAliasesEntry<TicketOf<T>, M>, PhantomData<T>)
179	{
180		type MigratedData = (VersionedLocation, AuthorizedAliasesEntry<TicketOf<T>, M>);
181
182		fn needs_migration(&self, required_version: XcmVersion) -> bool {
183			self.0.identify_version() != required_version ||
184				self.1
185					.aliasers
186					.iter()
187					.any(|alias| alias.location.identify_version() != required_version)
188		}
189
190		fn try_migrate(
191			self,
192			required_version: XcmVersion,
193		) -> Result<Option<Self::MigratedData>, ()> {
194			if !self.needs_migration(required_version) {
195				return Ok(None);
196			}
197
198			let key = if self.0.identify_version() != required_version {
199				let Ok(converted_key) = self.0.clone().into_version(required_version) else {
200					return Err(());
201				};
202				converted_key
203			} else {
204				self.0.clone()
205			};
206
207			let mut new_aliases = BoundedVec::<OriginAliaser, M>::new();
208			let (aliasers, ticket) = (self.1.aliasers, self.1.ticket);
209			for alias in aliasers {
210				let OriginAliaser { mut location, expiry } = alias.clone();
211				if location.identify_version() != required_version {
212					location = location.into_version(required_version)?;
213				}
214				new_aliases.try_push(OriginAliaser { location, expiry }).map_err(|_| ())?;
215			}
216
217			Ok(Some((key, AuthorizedAliasesEntry { aliasers: new_aliases, ticket })))
218		}
219	}
220
221	impl<T: Config> Pallet<T> {
222		/// Migrates relevant data to the `required_xcm_version`.
223		pub(crate) fn migrate_data_to_xcm_version(
224			weight: &mut Weight,
225			required_xcm_version: XcmVersion,
226		) {
227			const LOG_TARGET: &str = "runtime::xcm::pallet_xcm::migrate_data_to_xcm_version";
228
229			// check and migrate `Queries`
230			let queries_to_migrate = Queries::<T>::iter().filter_map(|(id, data)| {
231				match data.try_migrate(required_xcm_version) {
232					Ok(Some(new_data)) => Some((id, new_data)),
233					Ok(None) => None,
234					Err(_) => {
235						tracing::error!(
236							target: LOG_TARGET,
237							?id,
238							?required_xcm_version,
239							"`Queries` cannot be migrated!"
240						);
241						None
242					},
243				}
244			});
245			for (id, new_data) in queries_to_migrate {
246				tracing::info!(
247					target: LOG_TARGET,
248					query_id = ?id,
249					?new_data,
250					"Migrating `Queries`"
251				);
252				Queries::<T>::insert(id, new_data);
253				weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
254			}
255
256			// check and migrate `LockedFungibles`
257			let locked_fungibles_to_migrate =
258				LockedFungibles::<T>::iter().filter_map(|(id, data)| {
259					match data.try_migrate(required_xcm_version) {
260						Ok(Some(new_data)) => Some((id, new_data)),
261						Ok(None) => None,
262						Err(_) => {
263							tracing::error!(
264								target: LOG_TARGET,
265								?id,
266								?required_xcm_version,
267								"`LockedFungibles` cannot be migrated!"
268							);
269							None
270						},
271					}
272				});
273			for (id, new_data) in locked_fungibles_to_migrate {
274				tracing::info!(
275					target: LOG_TARGET,
276					account_id = ?id,
277					?new_data,
278					"Migrating `LockedFungibles`"
279				);
280				LockedFungibles::<T>::insert(id, new_data);
281				weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
282			}
283
284			// check and migrate `RemoteLockedFungibles` - 1. step - just data
285			let remote_locked_fungibles_to_migrate =
286				RemoteLockedFungibles::<T>::iter().filter_map(|(id, data)| {
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_accrue(T::DbWeight::get().reads_writes(1, 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_accrue(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_accrue(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_accrue(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_accrue(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}