referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/scheduler/
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
17//! A module that is responsible for migration of storage.
18
19use super::*;
20use crate::on_demand;
21use frame_support::{
22	migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias,
23	traits::UncheckedOnRuntimeUpgrade, weights::Weight,
24};
25
26// Import V4 types - these will be used directly for decoding V3 storage since they're
27// binary-compatible
28use super::assigner_coretime::{CoreDescriptor, Schedule};
29
30/// V3 storage format - types and storage items before migration to V4.
31pub(super) mod v3 {
32	use super::*;
33	use frame_support::pallet_prelude::{OptionQuery, Twox256};
34
35	/// Assignment type used in V2 and V3 storage (before migration to V4).
36	#[derive(Encode, Decode, TypeInfo, Debug, Clone, PartialEq)]
37	pub(crate) enum Assignment {
38		/// A pool assignment (on-demand).
39		Pool { para_id: ParaId, core_index: CoreIndex },
40		/// A bulk assignment (from broker chain).
41		Bulk(ParaId),
42	}
43
44	impl Assignment {
45		pub fn para_id(&self) -> ParaId {
46			match self {
47				Self::Pool { para_id, .. } => *para_id,
48				Self::Bulk(para_id) => *para_id,
49			}
50		}
51	}
52
53	#[storage_alias]
54	pub(crate) type ClaimQueue<T: Config> =
55		StorageValue<Pallet<T>, BTreeMap<CoreIndex, VecDeque<Assignment>>, ValueQuery>;
56
57	/// Storage alias for the old CoreSchedules storage in the AssignerCoretime pallet.
58	///
59	/// NOTE: The pallet name must match the name used in the runtime's `construct_runtime!` macro.
60	/// In production runtimes (Polkadot/Kusama), this is named "CoretimeAssignmentProvider".
61	///
62	/// We can decode directly into V4 types (Schedule, PartsOf57600, etc.) because they're
63	/// binary-compatible with the V3 types - field visibility doesn't affect encoding.
64	#[storage_alias]
65	pub(crate) type CoreSchedules<T: Config> = StorageMap<
66		CoretimeAssignmentProvider,
67		Twox256,
68		(BlockNumberFor<T>, CoreIndex),
69		Schedule<BlockNumberFor<T>>,
70		OptionQuery,
71	>;
72
73	/// Storage alias for the old CoreDescriptors storage in the AssignerCoretime pallet.
74	///
75	/// NOTE: The pallet name must match the name used in the runtime's `construct_runtime!` macro.
76	/// In production runtimes (Polkadot/Kusama), this is named "CoretimeAssignmentProvider".
77	#[storage_alias]
78	pub(crate) type CoreDescriptors<T: Config> = StorageMap<
79		CoretimeAssignmentProvider,
80		Twox256,
81		CoreIndex,
82		CoreDescriptor<BlockNumberFor<T>>,
83		ValueQuery,
84	>;
85}
86
87/// Migration for consolidating coretime assignment scheduling into the Scheduler pallet.
88///
89/// V4 completes the transition to the new coretime scheduling model by moving all coretime-related
90/// storage from the deprecated top-level AssignerCoretime pallet into the Scheduler pallet.
91///
92/// Major changes in V4:
93/// - **Removes ClaimQueue storage**: The scheduler no longer maintains a claim queue. Assignment
94///   scheduling is now handled entirely through CoreSchedules managed by the assigner_coretime
95///   submodule.
96/// - **Migrates CoreSchedules**: Moves schedule assignments from the deprecated AssignerCoretime
97///   pallet to Scheduler pallet storage, following linked-list structure via `next_schedule`
98///   pointers.
99/// - **Migrates CoreDescriptors**: Moves core metadata (queue descriptors and current work state)
100///   from the deprecated pallet to Scheduler pallet storage.
101/// - **Upgrades storage hasher**: Changes from Twox256 (non-reversible) to Twox64Concat
102///   (reversible) for CoreSchedules, enabling efficient iteration and queries.
103/// - **Preserves on-demand orders**: Any Pool assignments remaining in ClaimQueue are migrated to
104///   the on-demand pallet queue to ensure no orders are lost.
105/// - **Drops bulk assignments**: Bulk assignments (from broker chain) in ClaimQueue are
106///   intentionally dropped as they will be rescheduled from CoreSchedules in the new model.
107///
108/// After this migration, the deprecated AssignerCoretime pallet becomes an empty stub that can be
109/// removed once all networks have upgraded.
110pub struct UncheckedMigrateToV4<T>(core::marker::PhantomData<T>);
111
112#[cfg(any(feature = "try-runtime", test))]
113impl<T: Config> UncheckedMigrateToV4<T> {
114	pub fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
115		// Count schedules and descriptors by enumerating cores
116		let num_cores = configuration::ActiveConfig::<T>::get().scheduler_params.num_cores;
117		let mut schedule_count = 0u32;
118		let mut descriptor_count = 0u32;
119
120		for core_idx in 0..num_cores {
121			let core_index = CoreIndex(core_idx);
122			let descriptor = v3::CoreDescriptors::<T>::get(core_index);
123
124			if descriptor.queue().is_some() || descriptor.current_work().is_some() {
125				descriptor_count += 1;
126
127				// Count schedules by following the queue and validate linked list integrity
128				if let Some(queue) = descriptor.queue() {
129					let mut current_block = Some(queue.first);
130					while let Some(block_number) = current_block {
131						let key = (block_number, core_index);
132						if let Some(schedule) = v3::CoreSchedules::<T>::get(key) {
133							schedule_count += 1;
134							current_block = schedule.next_schedule();
135						} else {
136							// Linked list is broken - log warning and stop traversing this chain
137							// The data was already lost before migration, failing here won't help
138							log::warn!(
139								target: super::LOG_TARGET,
140								"Broken linked list detected for core {:?} at block {:?}",
141								core_index,
142								block_number
143							);
144							break;
145						}
146					}
147				}
148			}
149		}
150
151		let claim_queue = v3::ClaimQueue::<T>::get();
152		let mut total_assignments = 0u32;
153		let mut pool_assignments = 0u32;
154
155		for (_core_idx, assignments) in claim_queue.iter() {
156			for assignment in assignments {
157				total_assignments = total_assignments.saturating_add(1);
158				if matches!(assignment, v3::Assignment::Pool { .. }) {
159					pool_assignments = pool_assignments.saturating_add(1);
160				}
161			}
162		}
163
164		log::info!(
165			target: super::LOG_TARGET,
166			"Before migration v4: {} CoreSchedules, {} CoreDescriptors, {} ClaimQueue assignments ({} pool, {} bulk)",
167			schedule_count,
168			descriptor_count,
169			total_assignments,
170			pool_assignments,
171			total_assignments.saturating_sub(pool_assignments)
172		);
173
174		// Sanity check: On production networks, we expect to find data to migrate
175		// If we find nothing, the storage alias pallet name is likely wrong
176		if descriptor_count == 0 && schedule_count == 0 {
177			log::error!(
178				target: super::LOG_TARGET,
179				"Migration found no data to migrate! This likely means the storage alias pallet name \
180				(CoretimeAssignmentProvider) doesn't match the actual pallet name in construct_runtime. \
181				Check the runtime's construct_runtime macro for the correct pallet name."
182			);
183			return Err("No data found to migrate - wrong pallet name in storage alias?".into());
184		}
185
186		Ok((schedule_count, descriptor_count, total_assignments, pool_assignments).encode())
187	}
188
189	pub fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
190		log::info!(target: super::LOG_TARGET, "Running post_upgrade() for v4");
191
192		let (
193			expected_schedule_count,
194			expected_descriptor_count,
195			total_assignments,
196			expected_pool_assignments,
197		): (u32, u32, u32, u32) =
198			Decode::decode(&mut &state[..]).map_err(|_| "Failed to decode pre_upgrade state")?;
199
200		// Verify old storage is cleaned up
201		ensure!(!v3::ClaimQueue::<T>::exists(), "ClaimQueue storage should have been removed");
202
203		// Check old CoreSchedules and CoreDescriptors are empty by enumerating cores
204		let num_cores = configuration::ActiveConfig::<T>::get().scheduler_params.num_cores;
205		for core_idx in 0..num_cores {
206			let core_index = CoreIndex(core_idx);
207
208			// Check descriptor is default/empty
209			let old_descriptor = v3::CoreDescriptors::<T>::get(core_index);
210			ensure!(
211				old_descriptor.queue().is_none() && old_descriptor.current_work().is_none(),
212				"Old CoreDescriptors should be empty"
213			);
214		}
215
216		// Verify new storage (Twox64Concat allows iteration)
217		let new_schedule_count = super::CoreSchedules::<T>::iter().count() as u32;
218		ensure!(
219			new_schedule_count == expected_schedule_count,
220			"CoreSchedules count mismatch after migration"
221		);
222
223		let new_descriptor_count = super::CoreDescriptors::<T>::get().len() as u32;
224		ensure!(
225			new_descriptor_count == expected_descriptor_count,
226			"CoreDescriptors count mismatch after migration"
227		);
228
229		log::info!(
230			target: super::LOG_TARGET,
231			"Successfully migrated v4: {} CoreSchedules, {} CoreDescriptors from AssignerCoretime to Scheduler; {} ClaimQueue assignments ({} pool pushed to on-demand)",
232			new_schedule_count,
233			new_descriptor_count,
234			total_assignments,
235			expected_pool_assignments
236		);
237
238		Ok(())
239	}
240}
241
242impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV4<T> {
243	fn on_runtime_upgrade() -> Weight {
244		let mut weight: Weight = Weight::zero();
245
246		// Get the actual number of cores from configuration
247		let num_cores = configuration::ActiveConfig::<T>::get().scheduler_params.num_cores;
248		weight.saturating_accrue(T::DbWeight::get().reads(1));
249
250		// Step 1 & 2: Migrate CoreDescriptors and CoreSchedules together by enumerating cores
251		let mut schedule_count = 0u64;
252		let mut descriptor_count = 0u64;
253		let mut new_descriptors: BTreeMap<CoreIndex, CoreDescriptor<BlockNumberFor<T>>> =
254			BTreeMap::new();
255
256		for core_idx in 0..num_cores {
257			let core_index = CoreIndex(core_idx);
258
259			// Take the descriptor for this core (read and remove in one operation)
260			let old_descriptor = v3::CoreDescriptors::<T>::take(core_index);
261			weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
262
263			// Check if this core has a non-default descriptor
264			if old_descriptor.queue().is_none() && old_descriptor.current_work().is_none() {
265				continue; // Skip empty/default descriptors
266			}
267
268			descriptor_count += 1;
269
270			// Migrate schedules for this core by following the queue linked list
271			if let Some(queue) = old_descriptor.queue() {
272				let mut current_block = Some(queue.first);
273
274				while let Some(block_number) = current_block {
275					let key = (block_number, core_index);
276
277					if let Some(schedule) = v3::CoreSchedules::<T>::take(key) {
278						schedule_count += 1;
279						weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
280
281						// Save next_schedule before moving schedule
282						let next = schedule.next_schedule();
283
284						// Insert into new storage with new hasher (Twox64Concat)
285						super::CoreSchedules::<T>::insert(key, schedule);
286						weight.saturating_accrue(T::DbWeight::get().writes(1));
287
288						// Move to next schedule in queue
289						current_block = next;
290					} else {
291						// Queue is broken or reached the end
292
293						log::error!(
294							target: super::LOG_TARGET,
295							"Next queue entry was missing - this is unexpected, (core, block): {:?}",
296							key,
297						);
298						break;
299					}
300				}
301			}
302
303			// Descriptor can be used as-is since types are binary-compatible
304			new_descriptors.insert(core_index, old_descriptor);
305		}
306
307		// Defensive check: verify descriptor count matches what we collected
308		if descriptor_count > 0 && new_descriptors.len() as u64 != descriptor_count {
309			log::error!(
310				target: super::LOG_TARGET,
311				"Descriptor count mismatch during migration: expected {} but got {}",
312				descriptor_count,
313				new_descriptors.len()
314			);
315		}
316
317		// Write all descriptors at once
318		super::CoreDescriptors::<T>::put(new_descriptors);
319		weight.saturating_accrue(T::DbWeight::get().writes(1));
320
321		// Clear all old v3 storage completely to handle any orphaned entries
322		// (e.g., from previous higher num_cores or broken linked lists)
323		let _ = v3::CoreDescriptors::<T>::clear(u32::MAX, None);
324		let _ = v3::CoreSchedules::<T>::clear(u32::MAX, None);
325		weight.saturating_accrue(T::DbWeight::get().writes(2));
326
327		// Step 3: Migrate ClaimQueue - preserve pool assignments
328		let old_claim_queue = v3::ClaimQueue::<T>::take();
329		weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
330
331		let mut total_assignments = 0u32;
332		let mut migrated_pool_assignments = 0u32;
333
334		// Extract and preserve only Pool (on-demand) assignments.
335		// Bulk assignments will be repopulated from the broker chain via CoreSchedules.
336		for (_core_idx, assignments) in old_claim_queue.iter() {
337			for assignment in assignments {
338				total_assignments = total_assignments.saturating_add(1);
339				if let v3::Assignment::Pool { para_id, .. } = assignment {
340					// Push the on-demand order back to the on-demand pallet.
341					// This ensures user-paid orders are not lost.
342					on_demand::Pallet::<T>::push_back_order(*para_id);
343					migrated_pool_assignments = migrated_pool_assignments.saturating_add(1);
344				}
345				// Bulk assignments are intentionally dropped - this is
346				// technically not fully correct, but will not matter in
347				// practice as virtually nobody is sharing cores right now
348				// and even if so, this lack in preciseness would hardly be
349				// noticable.
350			}
351		}
352
353		// Account for writes to on-demand storage for pool assignments
354		weight.saturating_accrue(T::DbWeight::get().writes(migrated_pool_assignments as u64));
355
356		log::info!(
357			target: super::LOG_TARGET,
358			"Migrated para scheduler storage to v4: {} CoreSchedules, {} CoreDescriptors migrated from AssignerCoretime to Scheduler; removed ClaimQueue ({} total assignments, {} pool assignments migrated to on-demand)",
359			schedule_count,
360			descriptor_count,
361			total_assignments,
362			migrated_pool_assignments
363		);
364
365		weight
366	}
367
368	#[cfg(feature = "try-runtime")]
369	fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::DispatchError> {
370		Self::pre_upgrade()
371	}
372
373	#[cfg(feature = "try-runtime")]
374	fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::DispatchError> {
375		Self::post_upgrade(state)
376	}
377}
378
379/// Migrate `V3` to `V4` of the storage format.
380pub type MigrateV3ToV4<T> = VersionedMigration<
381	3,
382	4,
383	UncheckedMigrateToV4<T>,
384	Pallet<T>,
385	<T as frame_system::Config>::DbWeight,
386>;
387
388#[cfg(test)]
389mod v4_tests {
390	use super::*;
391	use crate::{
392		configuration,
393		mock::{new_test_ext, MockGenesisConfig, System, Test},
394		on_demand, scheduler,
395	};
396	use alloc::collections::BTreeMap;
397	use frame_support::traits::StorageVersion;
398	use pallet_broker::CoreAssignment as BrokerCoreAssignment;
399	use polkadot_primitives::{CoreIndex, Id as ParaId};
400
401	use super::assigner_coretime::{
402		AssignmentState, CoreDescriptor, PartsOf57600, QueueDescriptor, Schedule, WorkState,
403	};
404
405	#[test]
406	fn basic_migration_works() {
407		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
408			// Setup configuration with 1 core
409			configuration::ActiveConfig::<Test>::mutate(|c| {
410				c.scheduler_params.num_cores = 1;
411			});
412
413			// Setup: Create old storage
414			let core = CoreIndex(0);
415			let block_number = 10u32;
416			let para_id = ParaId::from(1000);
417
418			// Create old schedule
419			let old_schedule = Schedule::new(
420				vec![(
421					BrokerCoreAssignment::Task(para_id.into()),
422					PartsOf57600::new_saturating(28800),
423				)],
424				Some(100u32),
425				None,
426			);
427
428			// Create old descriptor with queue pointing to this schedule
429			let old_descriptor = CoreDescriptor::new(
430				Some(QueueDescriptor { first: block_number, last: block_number }),
431				None,
432			);
433
434			// Write to old storage using storage aliases
435			v3::CoreSchedules::<Test>::insert((block_number, core), old_schedule);
436			v3::CoreDescriptors::<Test>::insert(core, old_descriptor);
437
438			// Set storage version to 3
439			StorageVersion::new(3).put::<super::Pallet<Test>>();
440
441			// Run migration with pre and post upgrade checks
442			let state =
443				UncheckedMigrateToV4::<Test>::pre_upgrade().expect("pre_upgrade should succeed");
444			let _weight = UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
445			UncheckedMigrateToV4::<Test>::post_upgrade(state).expect("post_upgrade should succeed");
446
447			// Verify new storage
448			let new_schedule = super::CoreSchedules::<Test>::get((block_number, core))
449				.expect("Schedule should be migrated");
450			assert_eq!(new_schedule.assignments().len(), 1);
451			assert_eq!(new_schedule.end_hint(), Some(100u32));
452			assert_eq!(new_schedule.next_schedule(), None);
453
454			let new_descriptors = super::CoreDescriptors::<Test>::get();
455			let new_descriptor = new_descriptors.get(&core).expect("Descriptor should be migrated");
456			assert!(new_descriptor.queue().is_some());
457			assert!(new_descriptor.current_work().is_none());
458
459			// Verify old storage is cleared
460			assert!(
461				v3::CoreSchedules::<Test>::get((block_number, core)).is_none(),
462				"Old CoreSchedules should be cleared"
463			);
464
465			// Verify old CoreDescriptor is cleared (should be default/empty after migration)
466			let old_descriptor = v3::CoreDescriptors::<Test>::get(core);
467			assert!(
468				old_descriptor.queue().is_none() && old_descriptor.current_work().is_none(),
469				"Old CoreDescriptor should be cleared after migration"
470			);
471		});
472	}
473
474	#[test]
475	fn multi_core_migration_works() {
476		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
477			// Setup configuration with 3 cores
478			configuration::ActiveConfig::<Test>::mutate(|c| {
479				c.scheduler_params.num_cores = 3;
480			});
481
482			let block_number = 10u32;
483
484			// Setup three cores with different configurations
485			for core_idx in 0..3 {
486				let core = CoreIndex(core_idx);
487				let para_id = ParaId::from(1000 + core_idx);
488
489				let old_schedule = Schedule::new(
490					vec![(
491						BrokerCoreAssignment::Task(para_id.into()),
492						PartsOf57600::new_saturating(57600),
493					)],
494					None,
495					None,
496				);
497
498				let old_descriptor = CoreDescriptor::new(
499					Some(QueueDescriptor { first: block_number, last: block_number }),
500					None,
501				);
502
503				v3::CoreSchedules::<Test>::insert((block_number, core), old_schedule);
504				v3::CoreDescriptors::<Test>::insert(core, old_descriptor);
505			}
506
507			StorageVersion::new(3).put::<super::Pallet<Test>>();
508
509			// Run migration with pre and post upgrade checks
510			let state =
511				UncheckedMigrateToV4::<Test>::pre_upgrade().expect("pre_upgrade should succeed");
512			UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
513			UncheckedMigrateToV4::<Test>::post_upgrade(state).expect("post_upgrade should succeed");
514
515			// Verify all cores migrated
516			let new_descriptors = super::CoreDescriptors::<Test>::get();
517			assert_eq!(new_descriptors.len(), 3);
518
519			for core_idx in 0..3 {
520				let core = CoreIndex(core_idx);
521				assert!(new_descriptors.contains_key(&core));
522				assert!(super::CoreSchedules::<Test>::get((block_number, core)).is_some());
523			}
524		});
525	}
526
527	#[test]
528	fn linked_list_migration_works() {
529		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
530			// Setup configuration with 1 core
531			configuration::ActiveConfig::<Test>::mutate(|c| {
532				c.scheduler_params.num_cores = 1;
533			});
534
535			let core = CoreIndex(0);
536			let para_id = ParaId::from(1000);
537
538			// Create a linked list: block 10 -> 20 -> 30
539			let schedule_30 = Schedule::new(
540				vec![(
541					BrokerCoreAssignment::Task(para_id.into()),
542					PartsOf57600::new_saturating(57600),
543				)],
544				None,
545				None,
546			);
547
548			let schedule_20 = Schedule::new(
549				vec![(
550					BrokerCoreAssignment::Task(para_id.into()),
551					PartsOf57600::new_saturating(57600),
552				)],
553				None,
554				Some(30u32),
555			);
556
557			let schedule_10 = Schedule::new(
558				vec![(
559					BrokerCoreAssignment::Task(para_id.into()),
560					PartsOf57600::new_saturating(57600),
561				)],
562				None,
563				Some(20u32),
564			);
565
566			// Write schedules
567			v3::CoreSchedules::<Test>::insert((10u32, core), schedule_10);
568			v3::CoreSchedules::<Test>::insert((20u32, core), schedule_20);
569			v3::CoreSchedules::<Test>::insert((30u32, core), schedule_30);
570
571			// Descriptor points to first schedule
572			let old_descriptor =
573				CoreDescriptor::new(Some(QueueDescriptor { first: 10u32, last: 30u32 }), None);
574
575			v3::CoreDescriptors::<Test>::insert(core, old_descriptor);
576
577			StorageVersion::new(3).put::<super::Pallet<Test>>();
578
579			// Run migration with pre and post upgrade checks
580			let state =
581				UncheckedMigrateToV4::<Test>::pre_upgrade().expect("pre_upgrade should succeed");
582			UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
583			UncheckedMigrateToV4::<Test>::post_upgrade(state).expect("post_upgrade should succeed");
584
585			// Verify all three schedules migrated
586			assert!(super::CoreSchedules::<Test>::get((10u32, core)).is_some());
587			assert!(super::CoreSchedules::<Test>::get((20u32, core)).is_some());
588			assert!(super::CoreSchedules::<Test>::get((30u32, core)).is_some());
589
590			// Verify next_schedule links preserved
591			let new_10 = super::CoreSchedules::<Test>::get((10u32, core)).unwrap();
592			assert_eq!(new_10.next_schedule(), Some(20u32));
593
594			let new_20 = super::CoreSchedules::<Test>::get((20u32, core)).unwrap();
595			assert_eq!(new_20.next_schedule(), Some(30u32));
596
597			let new_30 = super::CoreSchedules::<Test>::get((30u32, core)).unwrap();
598			assert_eq!(new_30.next_schedule(), None);
599		});
600	}
601
602	#[test]
603	fn claim_queue_migration_works() {
604		// This test covers both pool and bulk assignment handling during ClaimQueue migration:
605		// - Pool assignments should be pushed to the on-demand pallet queue
606		// - Bulk assignments should be dropped (they will be rescheduled from CoreSchedules)
607		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
608			// Setup configuration with 1 core
609			configuration::ActiveConfig::<Test>::mutate(|c| {
610				c.scheduler_params.num_cores = 1;
611			});
612
613			let core = CoreIndex(0);
614			let pool_para_1 = ParaId::from(1000);
615			let pool_para_2 = ParaId::from(1001);
616			let bulk_para_claimqueue = ParaId::from(2000);
617			let bulk_para_descriptor = ParaId::from(3000);
618			let block_number = 10u32;
619
620			// Create a CoreDescriptor and Schedule (production networks will have these)
621			let descriptor = CoreDescriptor::new(
622				Some(QueueDescriptor { first: block_number, last: block_number }),
623				None,
624			);
625			v3::CoreDescriptors::<Test>::insert(core, descriptor);
626
627			// Schedule contains the authoritative bulk assignment
628			let schedule = Schedule::new(
629				vec![(
630					BrokerCoreAssignment::Task(bulk_para_descriptor.into()),
631					PartsOf57600::new_saturating(57600),
632				)],
633				None,
634				None,
635			);
636			v3::CoreSchedules::<Test>::insert((block_number, core), schedule);
637
638			// Create ClaimQueue with mixed assignments to test migration behavior:
639			// - Pool assignments (para 1000, 1001) should be migrated to on-demand queue
640			// - Bulk assignment (para 2000) should be dropped during migration
641			let mut claim_queue = BTreeMap::new();
642			let mut assignments = VecDeque::new();
643			assignments.push_back(v3::Assignment::Pool { para_id: pool_para_1, core_index: core });
644			assignments.push_back(v3::Assignment::Bulk(bulk_para_claimqueue)); // Will be dropped
645			assignments.push_back(v3::Assignment::Pool { para_id: pool_para_2, core_index: core });
646			claim_queue.insert(core, assignments);
647
648			v3::ClaimQueue::<Test>::put(claim_queue);
649
650			StorageVersion::new(3).put::<super::Pallet<Test>>();
651
652			// Verify claim_queue() returns the old ClaimQueue before migration
653			let claim_queue_before = super::Pallet::<Test>::claim_queue();
654			assert_eq!(claim_queue_before.len(), 1, "Should have 1 core in claim queue");
655			let core_queue = claim_queue_before.get(&core).expect("Core should be in claim queue");
656			assert_eq!(core_queue.len(), 3, "Core should have 3 assignments");
657			assert_eq!(core_queue[0], pool_para_1);
658			assert_eq!(core_queue[1], bulk_para_claimqueue);
659			assert_eq!(core_queue[2], pool_para_2);
660
661			// Run migration with pre and post upgrade checks
662			let state =
663				UncheckedMigrateToV4::<Test>::pre_upgrade().expect("pre_upgrade should succeed");
664			UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
665			UncheckedMigrateToV4::<Test>::post_upgrade(state).expect("post_upgrade should succeed");
666
667			// Verify ClaimQueue is removed
668			assert!(!v3::ClaimQueue::<Test>::exists());
669
670			// Test 1: Verify pool assignments went to on-demand
671			// The migration calls `on_demand::Pallet::<T>::push_back_order` for each pool
672			// assignment, which adds them to the on-demand queue. We verify by popping
673			// assignments. Orders are ready 2 blocks after being placed (asynchronous backing).
674			let mut on_demand_queue = on_demand::Pallet::<Test>::peek_order_queue();
675			let now = System::block_number().saturating_add(2); // Advance 2 blocks for async backing
676			let popped: Vec<ParaId> =
677				on_demand_queue.pop_assignment_for_cores::<Test>(now, 2).collect();
678
679			assert_eq!(popped.len(), 2, "Should have 2 pool assignments in on-demand queue");
680			assert!(popped.contains(&pool_para_1), "pool_para_1 should be in queue");
681			assert!(popped.contains(&pool_para_2), "pool_para_2 should be in queue");
682
683			// Test 2: Verify bulk assignments from ClaimQueue were dropped
684			// and the authoritative descriptor assignments are used instead.
685			// Advance to the block where the schedule becomes active.
686			frame_system::Pallet::<Test>::set_block_number(block_number);
687			let peeked = scheduler::assigner_coretime::peek_next_block::<Test>(10);
688			let core_assignments = peeked.get(&core).expect("Core should have assignments");
689			let para_ids: Vec<ParaId> = core_assignments.iter().copied().collect();
690
691			// Should see para from descriptor, not from claimqueue
692			assert!(
693				para_ids.contains(&bulk_para_descriptor),
694				"Should contain bulk para from descriptor"
695			);
696			assert!(
697				!para_ids.contains(&bulk_para_claimqueue),
698				"Should NOT contain bulk para from old ClaimQueue"
699			);
700		});
701	}
702
703	#[test]
704	fn empty_storage_migration_works() {
705		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
706			// No old storage created
707			StorageVersion::new(3).put::<super::Pallet<Test>>();
708
709			// pre_upgrade should fail if no data is found (sanity check for wrong pallet name)
710			assert!(UncheckedMigrateToV4::<Test>::pre_upgrade().is_err());
711
712			// But on_runtime_upgrade should still work (idempotent, safe to run)
713			let _weight = UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
714
715			// Verify new storage is empty
716			let new_descriptors = super::CoreDescriptors::<Test>::get();
717			assert!(new_descriptors.is_empty());
718
719			// Manually construct the expected state: (schedule_count, descriptor_count,
720			// total_assignments, pool_assignments)
721			let state = (0u32, 0u32, 0u32, 0u32).encode();
722			UncheckedMigrateToV4::<Test>::post_upgrade(state)
723				.expect("post_upgrade should succeed with empty state");
724		});
725	}
726
727	#[test]
728	fn parts_of_57600_conversion_works() {
729		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
730			// Setup configuration with 1 core
731			configuration::ActiveConfig::<Test>::mutate(|c| {
732				c.scheduler_params.num_cores = 1;
733			});
734
735			let core = CoreIndex(0);
736			let block_number = 10u32;
737			let para_id = ParaId::from(1000);
738
739			// Create schedule with various PartsOf57600 values
740			let old_schedule = Schedule::new(
741				vec![
742					(
743						BrokerCoreAssignment::Task(para_id.into()),
744						PartsOf57600::new_saturating(14400), // 1/4
745					),
746					(
747						BrokerCoreAssignment::Task(ParaId::from(1001).into()),
748						PartsOf57600::new_saturating(28800), // 1/2
749					),
750					(
751						BrokerCoreAssignment::Task(ParaId::from(1002).into()),
752						PartsOf57600::new_saturating(14400), // 1/4
753					),
754				],
755				None,
756				None,
757			);
758
759			let old_descriptor = CoreDescriptor::new(
760				Some(QueueDescriptor { first: block_number, last: block_number }),
761				None,
762			);
763
764			v3::CoreSchedules::<Test>::insert((block_number, core), old_schedule);
765			v3::CoreDescriptors::<Test>::insert(core, old_descriptor);
766
767			StorageVersion::new(3).put::<super::Pallet<Test>>();
768
769			// Run migration with pre and post upgrade checks
770			let state =
771				UncheckedMigrateToV4::<Test>::pre_upgrade().expect("pre_upgrade should succeed");
772			UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
773			UncheckedMigrateToV4::<Test>::post_upgrade(state).expect("post_upgrade should succeed");
774
775			// Verify assignments and their parts converted correctly
776			let new_schedule = super::CoreSchedules::<Test>::get((block_number, core))
777				.expect("Schedule should be migrated");
778			assert_eq!(new_schedule.assignments().len(), 3);
779
780			// Check sum of parts equals full allocation (14400 + 28800 + 14400 = 57600)
781			let sum: u16 = new_schedule.assignments().iter().map(|(_, parts)| parts.value()).sum();
782			assert_eq!(sum, 57600, "Sum of parts should equal full allocation");
783		});
784	}
785
786	#[test]
787	fn current_work_state_migrated() {
788		new_test_ext(MockGenesisConfig::default()).execute_with(|| {
789			// Setup configuration with 1 core
790			configuration::ActiveConfig::<Test>::mutate(|c| {
791				c.scheduler_params.num_cores = 1;
792			});
793
794			let core = CoreIndex(0);
795			let para_id = ParaId::from(1000);
796
797			// Create descriptor with current_work
798			let old_descriptor = CoreDescriptor::new(
799				None,
800				Some(WorkState {
801					assignments: vec![(
802						BrokerCoreAssignment::Task(para_id.into()),
803						AssignmentState {
804							ratio: PartsOf57600::new_saturating(57600),
805							remaining: PartsOf57600::new_saturating(28800),
806						},
807					)],
808					end_hint: Some(100u32),
809					pos: 0,
810					step: PartsOf57600::new_saturating(1),
811				}),
812			);
813
814			v3::CoreDescriptors::<Test>::insert(core, old_descriptor);
815
816			StorageVersion::new(3).put::<super::Pallet<Test>>();
817
818			// Run migration with pre and post upgrade checks
819			let state =
820				UncheckedMigrateToV4::<Test>::pre_upgrade().expect("pre_upgrade should succeed");
821			UncheckedMigrateToV4::<Test>::on_runtime_upgrade();
822			UncheckedMigrateToV4::<Test>::post_upgrade(state).expect("post_upgrade should succeed");
823
824			// Verify current_work migrated
825			let new_descriptors = super::CoreDescriptors::<Test>::get();
826			let new_descriptor = new_descriptors.get(&core).expect("Descriptor should exist");
827			assert!(new_descriptor.current_work().is_some());
828
829			let work = new_descriptor.current_work().unwrap();
830			assert_eq!(work.assignments.len(), 1);
831
832			// Verify the assignment details match what we set up
833			let (assignment, state) = &work.assignments[0];
834			match assignment {
835				BrokerCoreAssignment::Task(task_id) => {
836					assert_eq!(ParaId::from(*task_id), para_id, "ParaId should match");
837				},
838				_ => panic!("Expected Task assignment"),
839			}
840
841			// Verify assignment state values
842			assert_eq!(state.ratio.value(), 57600, "Ratio should be full allocation");
843			assert_eq!(state.remaining.value(), 28800, "Remaining should be half");
844
845			// Verify work state metadata
846			assert_eq!(work.end_hint, Some(100u32));
847			assert_eq!(work.pos, 0);
848			assert_eq!(work.step.value(), 1, "Step should be 1");
849		});
850	}
851}