referrerpolicy=no-referrer-when-downgrade

pallet_scheduler/
migration.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
18//! Migrations for the scheduler pallet.
19
20use super::*;
21use frame_support::traits::OnRuntimeUpgrade;
22
23#[cfg(feature = "try-runtime")]
24use sp_runtime::TryRuntimeError;
25
26/// The log target.
27const TARGET: &'static str = "runtime::scheduler::migration";
28
29pub mod v1 {
30	use super::*;
31	use frame_support::pallet_prelude::*;
32
33	#[frame_support::storage_alias]
34	pub(crate) type Agenda<T: Config> = StorageMap<
35		Pallet<T>,
36		Twox64Concat,
37		BlockNumberFor<T>,
38		Vec<Option<ScheduledV1<<T as Config>::RuntimeCall, BlockNumberFor<T>>>>,
39		ValueQuery,
40	>;
41
42	#[frame_support::storage_alias]
43	pub(crate) type Lookup<T: Config> =
44		StorageMap<Pallet<T>, Twox64Concat, Vec<u8>, TaskAddress<BlockNumberFor<T>>>;
45}
46
47pub mod v2 {
48	use super::*;
49	use frame_support::pallet_prelude::*;
50
51	#[frame_support::storage_alias]
52	pub(crate) type Agenda<T: Config> = StorageMap<
53		Pallet<T>,
54		Twox64Concat,
55		BlockNumberFor<T>,
56		Vec<Option<ScheduledV2Of<T>>>,
57		ValueQuery,
58	>;
59
60	#[frame_support::storage_alias]
61	pub(crate) type Lookup<T: Config> =
62		StorageMap<Pallet<T>, Twox64Concat, Vec<u8>, TaskAddress<BlockNumberFor<T>>>;
63}
64
65pub mod v3 {
66	use super::*;
67	use frame_support::pallet_prelude::*;
68
69	#[frame_support::storage_alias]
70	pub(crate) type Agenda<T: Config> = StorageMap<
71		Pallet<T>,
72		Twox64Concat,
73		BlockNumberFor<T>,
74		Vec<Option<ScheduledV3Of<T>>>,
75		ValueQuery,
76	>;
77
78	#[frame_support::storage_alias]
79	pub(crate) type Lookup<T: Config> =
80		StorageMap<Pallet<T>, Twox64Concat, Vec<u8>, TaskAddress<BlockNumberFor<T>>>;
81
82	/// Migrate the scheduler pallet from V3 to V4.
83	pub struct MigrateToV4<T>(core::marker::PhantomData<T>);
84
85	impl<T: Config> OnRuntimeUpgrade for MigrateToV4<T> {
86		#[cfg(feature = "try-runtime")]
87		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
88			ensure!(StorageVersion::get::<Pallet<T>>() == 3, "Can only upgrade from version 3");
89
90			let agendas = Agenda::<T>::iter_keys().count() as u32;
91			let decodable_agendas = Agenda::<T>::iter_values().count() as u32;
92			if agendas != decodable_agendas {
93				// This is not necessarily an error, but can happen when there are Calls
94				// in an Agenda that are not valid anymore with the new runtime.
95				log::error!(
96					target: TARGET,
97					"Can only decode {} of {} agendas - others will be dropped",
98					decodable_agendas,
99					agendas
100				);
101			}
102			log::info!(target: TARGET, "Trying to migrate {} agendas...", decodable_agendas);
103
104			// Check that no agenda overflows `MaxScheduledPerBlock`.
105			let max_scheduled_per_block = T::MaxScheduledPerBlock::get() as usize;
106			for (block_number, agenda) in Agenda::<T>::iter() {
107				if agenda.iter().cloned().flatten().count() > max_scheduled_per_block {
108					log::error!(
109						target: TARGET,
110						"Would truncate agenda of block {:?} from {} items to {} items.",
111						block_number,
112						agenda.len(),
113						max_scheduled_per_block,
114					);
115					return Err("Agenda would overflow `MaxScheduledPerBlock`.".into())
116				}
117			}
118			// Check that bounding the calls will not overflow `MAX_LENGTH`.
119			let max_length = T::Preimages::MAX_LENGTH as usize;
120			for (block_number, agenda) in Agenda::<T>::iter() {
121				for schedule in agenda.iter().cloned().flatten() {
122					match schedule.call {
123						frame_support::traits::schedule::MaybeHashed::Value(call) => {
124							let l = call.using_encoded(|c| c.len());
125							if l > max_length {
126								log::error!(
127									target: TARGET,
128									"Call in agenda of block {:?} is too large: {} byte",
129									block_number,
130									l,
131								);
132								return Err("Call is too large.".into())
133							}
134						},
135						_ => (),
136					}
137				}
138			}
139
140			Ok((decodable_agendas as u32).encode())
141		}
142
143		fn on_runtime_upgrade() -> Weight {
144			let version = StorageVersion::get::<Pallet<T>>();
145			if version != 3 {
146				log::warn!(
147					target: TARGET,
148					"skipping v3 to v4 migration: executed on wrong storage version.\
149				Expected version 3, found {:?}",
150					version,
151				);
152				return T::DbWeight::get().reads(1)
153			}
154
155			crate::Pallet::<T>::migrate_v3_to_v4()
156		}
157
158		#[cfg(feature = "try-runtime")]
159		fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> {
160			ensure!(StorageVersion::get::<Pallet<T>>() == 4, "Must upgrade");
161
162			// Check that everything decoded fine.
163			for k in crate::Agenda::<T>::iter_keys() {
164				ensure!(crate::Agenda::<T>::try_get(k).is_ok(), "Cannot decode V4 Agenda");
165			}
166
167			let old_agendas: u32 =
168				Decode::decode(&mut &state[..]).expect("pre_upgrade provides a valid state; qed");
169			let new_agendas = crate::Agenda::<T>::iter_keys().count() as u32;
170			if old_agendas != new_agendas {
171				// This is not necessarily an error, but can happen when there are Calls
172				// in an Agenda that are not valid anymore in the new runtime.
173				log::error!(
174					target: TARGET,
175					"Did not migrate all Agendas. Previous {}, Now {}",
176					old_agendas,
177					new_agendas,
178				);
179			} else {
180				log::info!(target: TARGET, "Migrated {} agendas.", new_agendas);
181			}
182
183			Ok(())
184		}
185	}
186}
187
188pub mod v4 {
189	use super::*;
190	use frame_support::pallet_prelude::*;
191
192	/// This migration cleans up empty agendas of the V4 scheduler.
193	///
194	/// This should be run on a scheduler that does not have
195	/// <https://github.com/paritytech/substrate/pull/12989> since it piles up `None`-only agendas. This does not modify the pallet version.
196	pub struct CleanupAgendas<T>(core::marker::PhantomData<T>);
197
198	impl<T: Config> OnRuntimeUpgrade for CleanupAgendas<T> {
199		#[cfg(feature = "try-runtime")]
200		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
201			assert_eq!(
202				StorageVersion::get::<Pallet<T>>(),
203				4,
204				"Can only cleanup agendas of the V4 scheduler"
205			);
206
207			let agendas = Agenda::<T>::iter_keys().count();
208			let non_empty_agendas =
209				Agenda::<T>::iter_values().filter(|a| a.iter().any(|s| s.is_some())).count();
210			log::info!(
211				target: TARGET,
212				"There are {} total and {} non-empty agendas",
213				agendas,
214				non_empty_agendas
215			);
216
217			Ok((agendas as u32, non_empty_agendas as u32).encode())
218		}
219
220		fn on_runtime_upgrade() -> Weight {
221			let version = StorageVersion::get::<Pallet<T>>();
222			if version != 4 {
223				log::warn!(target: TARGET, "Skipping CleanupAgendas migration since it was run on the wrong version: {:?} != 4", version);
224				return T::DbWeight::get().reads(1)
225			}
226
227			let keys = Agenda::<T>::iter_keys().collect::<Vec<_>>();
228			let mut writes = 0;
229			for k in &keys {
230				let mut schedules = Agenda::<T>::get(k);
231				let all_schedules = schedules.len();
232				let suffix_none_schedules =
233					schedules.iter().rev().take_while(|s| s.is_none()).count();
234
235				match all_schedules.checked_sub(suffix_none_schedules) {
236					Some(0) => {
237						log::info!(
238							"Deleting None-only agenda {:?} with {} entries",
239							k,
240							all_schedules
241						);
242						Agenda::<T>::remove(k);
243						writes.saturating_inc();
244					},
245					Some(ne) if ne > 0 => {
246						log::info!(
247							"Removing {} schedules of {} from agenda {:?}, now {:?}",
248							suffix_none_schedules,
249							all_schedules,
250							ne,
251							k
252						);
253						schedules.truncate(ne);
254						Agenda::<T>::insert(k, schedules);
255						writes.saturating_inc();
256					},
257					Some(_) => {
258						frame_support::defensive!(
259							// Bad but let's not panic.
260							"Cannot have more None suffix schedules that schedules in total"
261						);
262					},
263					None => {
264						log::info!("Agenda {:?} does not have any None suffix schedules", k);
265					},
266				}
267			}
268
269			// We don't modify the pallet version.
270
271			T::DbWeight::get().reads_writes(1 + keys.len().saturating_mul(2) as u64, writes)
272		}
273
274		#[cfg(feature = "try-runtime")]
275		fn post_upgrade(state: Vec<u8>) -> Result<(), TryRuntimeError> {
276			ensure!(StorageVersion::get::<Pallet<T>>() == 4, "Version must not change");
277
278			let (old_agendas, non_empty_agendas): (u32, u32) =
279				Decode::decode(&mut state.as_ref()).expect("Must decode pre_upgrade state");
280			let new_agendas = Agenda::<T>::iter_keys().count() as u32;
281
282			match old_agendas.checked_sub(new_agendas) {
283				Some(0) => log::warn!(
284					target: TARGET,
285					"Did not clean up any agendas. v4::CleanupAgendas can be removed."
286				),
287				Some(n) => {
288					log::info!(target: TARGET, "Cleaned up {} agendas, now {}", n, new_agendas)
289				},
290				None => unreachable!(
291					"Number of agendas cannot increase, old {} new {}",
292					old_agendas, new_agendas
293				),
294			}
295			ensure!(new_agendas == non_empty_agendas, "Expected to keep all non-empty agendas");
296
297			Ok(())
298		}
299	}
300}
301
302#[cfg(test)]
303#[cfg(feature = "try-runtime")]
304mod test {
305	use super::*;
306	use crate::mock::*;
307	use alloc::borrow::Cow;
308	use frame_support::Hashable;
309	use substrate_test_utils::assert_eq_uvec;
310
311	#[test]
312	#[allow(deprecated)]
313	fn migration_v3_to_v4_works() {
314		new_test_ext().execute_with(|| {
315			// Assume that we are at V3.
316			StorageVersion::new(3).put::<Scheduler>();
317
318			// Call that will be bounded to a `Lookup`.
319			let large_call =
320				RuntimeCall::System(frame_system::Call::remark { remark: vec![0; 1024] });
321			// Call that can be inlined.
322			let small_call =
323				RuntimeCall::System(frame_system::Call::remark { remark: vec![0; 10] });
324			// Call that is already hashed and can will be converted to `Legacy`.
325			let hashed_call =
326				RuntimeCall::System(frame_system::Call::remark { remark: vec![0; 2048] });
327			let bound_hashed_call = Preimage::bound(hashed_call.clone()).unwrap();
328			assert!(bound_hashed_call.lookup_needed());
329			// A Call by hash that will fail to decode becomes `None`.
330			let trash_data = vec![255u8; 1024];
331			let undecodable_hash = Preimage::note(Cow::Borrowed(&trash_data)).unwrap();
332
333			for i in 0..2u64 {
334				let k = i.twox_64_concat();
335				let old = vec![
336					Some(ScheduledV3Of::<Test> {
337						maybe_id: None,
338						priority: i as u8 + 10,
339						call: small_call.clone().into(),
340						maybe_periodic: None, // 1
341						origin: root(),
342						_phantom: PhantomData::<u64>::default(),
343					}),
344					None,
345					Some(ScheduledV3Of::<Test> {
346						maybe_id: Some(vec![i as u8; 32]),
347						priority: 123,
348						call: large_call.clone().into(),
349						maybe_periodic: Some((4u64, 20)),
350						origin: signed(i),
351						_phantom: PhantomData::<u64>::default(),
352					}),
353					Some(ScheduledV3Of::<Test> {
354						maybe_id: Some(vec![255 - i as u8; 320]),
355						priority: 123,
356						call: MaybeHashed::Hash(bound_hashed_call.hash()),
357						maybe_periodic: Some((8u64, 10)),
358						origin: signed(i),
359						_phantom: PhantomData::<u64>::default(),
360					}),
361					Some(ScheduledV3Of::<Test> {
362						maybe_id: Some(vec![i as u8; 320]),
363						priority: 123,
364						call: MaybeHashed::Hash(undecodable_hash),
365						maybe_periodic: Some((4u64, 20)),
366						origin: root(),
367						_phantom: PhantomData::<u64>::default(),
368					}),
369				];
370				frame_support::migration::put_storage_value(b"Scheduler", b"Agenda", &k, old);
371			}
372
373			let state = v3::MigrateToV4::<Test>::pre_upgrade().unwrap();
374			let _w = v3::MigrateToV4::<Test>::on_runtime_upgrade();
375			v3::MigrateToV4::<Test>::post_upgrade(state).unwrap();
376
377			let mut x = Agenda::<Test>::iter().map(|x| (x.0, x.1.into_inner())).collect::<Vec<_>>();
378			x.sort_by_key(|x| x.0);
379
380			let bound_large_call = Preimage::bound(large_call).unwrap();
381			assert!(bound_large_call.lookup_needed());
382			let bound_small_call = Preimage::bound(small_call).unwrap();
383			assert!(!bound_small_call.lookup_needed());
384
385			let expected = vec![
386				(
387					0,
388					vec![
389						Some(ScheduledOf::<Test> {
390							maybe_id: None,
391							priority: 10,
392							call: bound_small_call.clone(),
393							maybe_periodic: None,
394							origin: root(),
395							_phantom: PhantomData::<u64>::default(),
396						}),
397						None,
398						Some(ScheduledOf::<Test> {
399							maybe_id: Some(blake2_256(&[0u8; 32])),
400							priority: 123,
401							call: bound_large_call.clone(),
402							maybe_periodic: Some((4u64, 20)),
403							origin: signed(0),
404							_phantom: PhantomData::<u64>::default(),
405						}),
406						Some(ScheduledOf::<Test> {
407							maybe_id: Some(blake2_256(&[255u8; 320])),
408							priority: 123,
409							call: Bounded::from_legacy_hash(bound_hashed_call.hash()),
410							maybe_periodic: Some((8u64, 10)),
411							origin: signed(0),
412							_phantom: PhantomData::<u64>::default(),
413						}),
414						None,
415					],
416				),
417				(
418					1,
419					vec![
420						Some(ScheduledOf::<Test> {
421							maybe_id: None,
422							priority: 11,
423							call: bound_small_call.clone(),
424							maybe_periodic: None,
425							origin: root(),
426							_phantom: PhantomData::<u64>::default(),
427						}),
428						None,
429						Some(ScheduledOf::<Test> {
430							maybe_id: Some(blake2_256(&[1u8; 32])),
431							priority: 123,
432							call: bound_large_call.clone(),
433							maybe_periodic: Some((4u64, 20)),
434							origin: signed(1),
435							_phantom: PhantomData::<u64>::default(),
436						}),
437						Some(ScheduledOf::<Test> {
438							maybe_id: Some(blake2_256(&[254u8; 320])),
439							priority: 123,
440							call: Bounded::from_legacy_hash(bound_hashed_call.hash()),
441							maybe_periodic: Some((8u64, 10)),
442							origin: signed(1),
443							_phantom: PhantomData::<u64>::default(),
444						}),
445						None,
446					],
447				),
448			];
449			for (outer, (i, j)) in x.iter().zip(expected.iter()).enumerate() {
450				assert_eq!(i.0, j.0);
451				for (inner, (x, y)) in i.1.iter().zip(j.1.iter()).enumerate() {
452					assert_eq!(x, y, "at index: outer {} inner {}", outer, inner);
453				}
454			}
455			assert_eq_uvec!(x, expected);
456
457			assert_eq!(StorageVersion::get::<Scheduler>(), 4);
458		});
459	}
460
461	#[test]
462	#[allow(deprecated)]
463	fn migration_v3_to_v4_too_large_calls_are_ignored() {
464		new_test_ext().execute_with(|| {
465			// Assume that we are at V3.
466			StorageVersion::new(3).put::<Scheduler>();
467
468			let too_large_call = RuntimeCall::System(frame_system::Call::remark {
469				remark: vec![0; <Test as Config>::Preimages::MAX_LENGTH + 1],
470			});
471
472			let i = 0u64;
473			let k = i.twox_64_concat();
474			let old = vec![Some(ScheduledV3Of::<Test> {
475				maybe_id: None,
476				priority: 1,
477				call: too_large_call.clone().into(),
478				maybe_periodic: None,
479				origin: root(),
480				_phantom: PhantomData::<u64>::default(),
481			})];
482			frame_support::migration::put_storage_value(b"Scheduler", b"Agenda", &k, old);
483
484			// The pre_upgrade hook fails:
485			let err = v3::MigrateToV4::<Test>::pre_upgrade().unwrap_err();
486			assert_eq!(DispatchError::from("Call is too large."), err);
487			// But the migration itself works:
488			let _w = v3::MigrateToV4::<Test>::on_runtime_upgrade();
489
490			let mut x = Agenda::<Test>::iter().map(|x| (x.0, x.1.into_inner())).collect::<Vec<_>>();
491			x.sort_by_key(|x| x.0);
492			// The call becomes `None`.
493			let expected = vec![(0, vec![None])];
494			assert_eq_uvec!(x, expected);
495
496			assert_eq!(StorageVersion::get::<Scheduler>(), 4);
497		});
498	}
499
500	#[test]
501	fn cleanup_agendas_works() {
502		use sp_core::bounded_vec;
503		new_test_ext().execute_with(|| {
504			StorageVersion::new(4).put::<Scheduler>();
505
506			let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] });
507			let bounded_call = Preimage::bound(call).unwrap();
508			let some = Some(ScheduledOf::<Test> {
509				maybe_id: None,
510				priority: 1,
511				call: bounded_call,
512				maybe_periodic: None,
513				origin: root(),
514				_phantom: Default::default(),
515			});
516
517			// Put some empty, and some non-empty agendas in there.
518			let test_data: Vec<(
519				BoundedVec<Option<ScheduledOf<Test>>, <Test as Config>::MaxScheduledPerBlock>,
520				Option<
521					BoundedVec<Option<ScheduledOf<Test>>, <Test as Config>::MaxScheduledPerBlock>,
522				>,
523			)> = vec![
524				(bounded_vec![some.clone()], Some(bounded_vec![some.clone()])),
525				(bounded_vec![None, some.clone()], Some(bounded_vec![None, some.clone()])),
526				(bounded_vec![None, some.clone(), None], Some(bounded_vec![None, some.clone()])),
527				(bounded_vec![some.clone(), None, None], Some(bounded_vec![some.clone()])),
528				(bounded_vec![None, None], None),
529				(bounded_vec![None, None, None], None),
530				(bounded_vec![], None),
531			];
532
533			// Insert all the agendas.
534			for (i, test) in test_data.iter().enumerate() {
535				Agenda::<Test>::insert(i as u64, test.0.clone());
536			}
537
538			// Run the migration.
539			let data = v4::CleanupAgendas::<Test>::pre_upgrade().unwrap();
540			let _w = v4::CleanupAgendas::<Test>::on_runtime_upgrade();
541			v4::CleanupAgendas::<Test>::post_upgrade(data).unwrap();
542
543			// Check that the post-state is correct.
544			for (i, test) in test_data.iter().enumerate() {
545				match test.1.clone() {
546					None => assert!(
547						!Agenda::<Test>::contains_key(i as u64),
548						"Agenda {} should be removed",
549						i
550					),
551					Some(new) => {
552						assert_eq!(Agenda::<Test>::get(i as u64), new, "Agenda wrong {}", i)
553					},
554				}
555			}
556		});
557	}
558
559	fn signed(i: u64) -> OriginCaller {
560		system::RawOrigin::Signed(i).into()
561	}
562}