referrerpolicy=no-referrer-when-downgrade

pallet_revive/migrations/
v4.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//! # Multi-Block Migration v4
19//!
20//! Switches storage deposits from the native currency to PGAS.
21//!
22//! Phase 1 iterates [`crate::CodeInfoOf`] and for each uploaded code records the uploader's
23//! existing native `CodeUploadDepositReserve` contribution under [`crate::NativeDepositOf`],
24//! keyed by the pallet's own account (the holder of that deposit). The native currency itself
25//! stays where it is.
26//!
27//! Phase 2 iterates [`crate::AccountInfoOf`] and for each contract burn the native
28//! `StorageDepositReserve` hold and replaces it with the same amount of PGAS minted into the
29//! contract and held under the same reason.
30//!
31//! Phase 3 rewrites the [`crate::DeletionQueue`] entries from their old `TrieId` value into the
32//! new [`crate::storage::DeletionQueueItem`] format.
33//!
34//! Phases 1 and 2 are skipped when [`crate::Config::Deposit`] does not support PGAS (i.e. the
35//! default `()` backend); only phase 3 runs in that case.
36
37use super::PALLET_MIGRATIONS_ID;
38#[cfg(feature = "try-runtime")]
39use crate::BalanceOf;
40use crate::{
41	AccountInfoOf, CodeInfoOf, Config, DeletionQueue, HoldReason, LOG_TARGET, NativeDepositOf,
42	Pallet, TrieId,
43	address::AddressMapper,
44	deposit_payment::Deposit,
45	storage::{AccountType, DeletionQueueItem},
46	weights::WeightInfo,
47};
48use codec::{Decode, Encode, MaxEncodedLen};
49use core::marker::PhantomData;
50use frame_support::{
51	Twox64Concat,
52	migrations::{MigrationId, SteppedMigration, SteppedMigrationError},
53	storage_alias,
54	weights::WeightMeter,
55};
56use scale_info::TypeInfo;
57use sp_core::{H160, H256};
58use sp_runtime::traits::{Saturating, TrailingZeroInput};
59
60extern crate alloc;
61
62#[cfg(feature = "try-runtime")]
63use alloc::{collections::btree_map::BTreeMap, vec::Vec};
64
65/// Three-phase cursor: code uploads, contracts, then deletion-queue rewrite.
66#[derive(Clone, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq, Debug)]
67pub enum Cursor {
68	/// Last code hash processed in phase 1 (`CodeInfoOf` iteration).
69	CodeUpload(H256),
70	/// Last contract address processed in phase 2 (`AccountInfoOf` iteration).
71	///
72	/// `None` is the transition sentinel from phase 1 to phase 2.
73	Contract(Option<H160>),
74	/// Last deletion-queue index processed in phase 3 (`DeletionQueue` rewrite).
75	///
76	/// `None` is the transition sentinel from phase 2 to phase 3.
77	DeletionQueue(Option<u32>),
78}
79
80/// Switches native storage deposits over to PGAS.
81pub struct Migration<T>(PhantomData<T>);
82
83impl<T: Config> SteppedMigration for Migration<T> {
84	type Cursor = Cursor;
85	type Identifier = MigrationId<17>;
86
87	fn id() -> Self::Identifier {
88		MigrationId { pallet_id: *PALLET_MIGRATIONS_ID, version_from: 3, version_to: 4 }
89	}
90
91	fn step(
92		mut cursor: Option<Self::Cursor>,
93		meter: &mut WeightMeter,
94	) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
95		let code_step = <T as Config>::WeightInfo::v4_code_upload_step();
96		let contract_step = <T as Config>::WeightInfo::v4_contract_step();
97		let deletion_queue_step = <T as Config>::WeightInfo::v4_deletion_queue_step();
98		let required = code_step.max(contract_step).max(deletion_queue_step);
99		if !meter.can_consume(required) {
100			return Err(SteppedMigrationError::InsufficientWeight { required });
101		}
102
103		loop {
104			let step_weight = match &cursor {
105				None | Some(Cursor::CodeUpload(_)) => code_step,
106				Some(Cursor::Contract(_)) => contract_step,
107				Some(Cursor::DeletionQueue(_)) => deletion_queue_step,
108			};
109			if meter.try_consume(step_weight).is_err() {
110				break;
111			}
112			cursor = Self::step_once(cursor);
113			if cursor.is_none() {
114				break;
115			}
116		}
117		Ok(cursor)
118	}
119
120	#[cfg(feature = "try-runtime")]
121	fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
122		use crate::deposit_payment::Deposit;
123
124		let mut per_owner: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
125		if T::Deposit::SUPPORTS_PGAS {
126			for (_hash, info) in CodeInfoOf::<T>::iter() {
127				let entry = per_owner.entry(info.owner().clone()).or_default();
128				*entry = entry.saturating_add(info.deposit());
129			}
130		}
131
132		let mut per_contract: BTreeMap<H160, BalanceOf<T>> = BTreeMap::new();
133		for (addr, info) in AccountInfoOf::<T>::iter() {
134			if !matches!(info.account_type, AccountType::Contract(_)) {
135				continue;
136			}
137			let contract = T::AddressMapper::to_account_id(&addr);
138			let total = T::Deposit::total_on_hold(HoldReason::StorageDepositReserve, &contract);
139			per_contract.insert(addr, total);
140		}
141
142		let deletion_queue: BTreeMap<u32, TrieId> = old::DeletionQueue::<T>::iter().collect();
143
144		Ok((per_owner, per_contract, deletion_queue).encode())
145	}
146
147	#[cfg(feature = "try-runtime")]
148	fn post_upgrade(prev: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
149		use crate::deposit_payment::Deposit;
150
151		let (per_owner, per_contract, deletion_queue) = <(
152			BTreeMap<T::AccountId, BalanceOf<T>>,
153			BTreeMap<H160, BalanceOf<T>>,
154			BTreeMap<u32, TrieId>,
155		)>::decode(&mut &prev[..])
156		.expect("Failed to decode pre_upgrade state");
157
158		// `NativeDepositOf` is introduced in this migration and starts empty.
159		let pallet_account = Pallet::<T>::account_id();
160		for (owner, expected) in per_owner {
161			let got = NativeDepositOf::<T>::get(&pallet_account, &owner);
162			assert_eq!(
163				got, expected,
164				"v4: NativeDepositOf[pallet][{owner:?}] = {got:?}, expected {expected:?}",
165			);
166		}
167
168		for (addr, expected) in per_contract {
169			let contract = T::AddressMapper::to_account_id(&addr);
170			let total = T::Deposit::total_on_hold(HoldReason::StorageDepositReserve, &contract);
171			assert_eq!(
172				total, expected,
173				"v4: contract {addr:?} total_on_hold changed: {total:?} != pre-migration {expected:?}",
174			);
175		}
176
177		let zero_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes())
178			.expect("zero input decodes to a valid AccountId; qed");
179		for (key, trie_id) in deletion_queue {
180			let got = DeletionQueue::<T>::get(key);
181			let expected = DeletionQueueItem::<T>::new(trie_id, zero_account.clone());
182			assert_eq!(
183				got,
184				Some(expected),
185				"v4: DeletionQueue[{key}] not rewritten into the new format",
186			);
187		}
188		Ok(())
189	}
190}
191
192/// Pre-v4 storage layouts.
193pub(crate) mod old {
194	use super::*;
195
196	/// Pre-v4 layout: a single [`TrieId`] per slot, no associated contract account. We only
197	/// iterate it; new entries are written via the live [`crate::DeletionQueue`].
198	#[storage_alias]
199	pub(crate) type DeletionQueue<T: Config> = StorageMap<Pallet<T>, Twox64Concat, u32, TrieId>;
200}
201
202impl<T: Config> Migration<T> {
203	/// Run a single iteration of the migration's inner loop, returning the next cursor or
204	/// `None` if the migration is complete.
205	pub(crate) fn step_once(cursor: Option<Cursor>) -> Option<Cursor> {
206		// Without PGAS support phases 1 and 2 are no-ops, so skip straight to phase 3.
207		// Forced on under `runtime-benchmarks` so per-phase weights are still measured.
208		if !T::Deposit::SUPPORTS_PGAS && !cfg!(feature = "runtime-benchmarks") {
209			return match cursor {
210				None | Some(Cursor::CodeUpload(_)) | Some(Cursor::Contract(_)) => {
211					Some(Cursor::DeletionQueue(None))
212				},
213				Some(Cursor::DeletionQueue(last)) => match Self::step_3_deletion_queue(last) {
214					Some(next) => Some(Cursor::DeletionQueue(Some(next))),
215					None => None,
216				},
217			};
218		}
219
220		match cursor {
221			None | Some(Cursor::CodeUpload(_)) => {
222				let last = if let Some(Cursor::CodeUpload(h)) = cursor { Some(h) } else { None };
223				Self::step_1_code_upload(last)
224			},
225			Some(Cursor::Contract(last)) => Some(match Self::step_2_contract(last) {
226				Some(next) => Cursor::Contract(Some(next)),
227				None => Cursor::DeletionQueue(None),
228			}),
229			Some(Cursor::DeletionQueue(last)) => match Self::step_3_deletion_queue(last) {
230				Some(next) => Some(Cursor::DeletionQueue(Some(next))),
231				None => None,
232			},
233		}
234	}
235
236	/// Phase 1: credit the next `CodeInfoOf` entry's owner in [`NativeDepositOf`]. Returns
237	/// `Some(Cursor::Contract(None))` when phase 1 is exhausted.
238	fn step_1_code_upload(last: Option<H256>) -> Option<Cursor> {
239		let mut iter = match last {
240			Some(last) => CodeInfoOf::<T>::iter_from(CodeInfoOf::<T>::hashed_key_for(last)),
241			None => CodeInfoOf::<T>::iter(),
242		};
243
244		let Some((hash, info)) = iter.next() else { return Some(Cursor::Contract(None)) };
245
246		let pallet_account = Pallet::<T>::account_id();
247		NativeDepositOf::<T>::mutate(&pallet_account, info.owner(), |entitlement| {
248			*entitlement = entitlement.saturating_add(info.deposit());
249		});
250		Some(Cursor::CodeUpload(hash))
251	}
252
253	/// Phase 2: hand the next contract to [`Deposit::migrate_native_to_pgas`]. EOAs are
254	/// skipped but still advance the cursor.
255	fn step_2_contract(last: Option<H160>) -> Option<H160> {
256		use frame_support::traits::fungible::InspectHold;
257
258		let mut iter = match last {
259			Some(last) => AccountInfoOf::<T>::iter_from(AccountInfoOf::<T>::hashed_key_for(last)),
260			None => AccountInfoOf::<T>::iter(),
261		};
262
263		let (addr, info) = iter.next()?;
264		if matches!(info.account_type, AccountType::Contract(_)) {
265			let contract = T::AddressMapper::to_account_id(&addr);
266			let held =
267				T::Currency::balance_on_hold(&HoldReason::StorageDepositReserve.into(), &contract);
268			if let Err(err) = T::Deposit::migrate_native_to_pgas(
269				HoldReason::StorageDepositReserve,
270				&contract,
271				held,
272			) {
273				log::error!(
274					target: LOG_TARGET,
275					"v4: failed to migrate native -> PGAS deposit for contract {addr:?}: {err:?}",
276				);
277			}
278		}
279		Some(addr)
280	}
281
282	/// Phase 3: rewrite the next [`DeletionQueue`] slot from the old `TrieId`-only layout
283	/// into the new [`DeletionQueueItem`] format. Pre-v4 entries had no [`NativeDepositOf`]
284	/// rows, so the recorded `account_id` is a zero placeholder; phase 1 of the deletion
285	/// processor will clear an empty prefix on it. Returns `None` when phase 3 finishes.
286	fn step_3_deletion_queue(last: Option<u32>) -> Option<u32> {
287		let mut iter = match last {
288			Some(last) => {
289				old::DeletionQueue::<T>::iter_from(old::DeletionQueue::<T>::hashed_key_for(last))
290			},
291			None => old::DeletionQueue::<T>::iter(),
292		};
293
294		let (key, trie_id) = iter.next()?;
295		// Same physical slot as `old::DeletionQueue`; the insert overwrites the legacy value
296		// with the new encoding.
297		let zero_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes())
298			.expect("zero input decodes to a valid AccountId; qed");
299		DeletionQueue::<T>::insert(key, DeletionQueueItem::<T>::new(trie_id, zero_account));
300		Some(key)
301	}
302}
303
304#[cfg(any(feature = "runtime-benchmarks", feature = "try-runtime", test))]
305impl<T: Config> Migration<T> {
306	/// Drive the migration to completion. Test/benchmark helper.
307	pub fn run_to_completion() {
308		let mut cursor: Option<Cursor> = None;
309		let mut meter = WeightMeter::new();
310		while let Ok(Some(next)) = <Self as SteppedMigration>::step(cursor, &mut meter) {
311			cursor = Some(next);
312		}
313	}
314}
315
316#[cfg(test)]
317mod tests {
318	use super::*;
319	use crate::{
320		CodeInfo, FreezeReason,
321		storage::{AccountInfo, ContractInfo},
322		tests::{Assets, AssetsFreezer, AssetsHolder, ExtBuilder, PGasAssetId, Test},
323	};
324	use frame_support::traits::fungible::{
325		Inspect as _, InspectHold as _, Mutate as _, MutateHold as _,
326	};
327	use sp_runtime::AccountId32;
328
329	type V4 = Migration<Test>;
330
331	fn seed_code_upload(hash: H256, owner: AccountId32, deposit: u128) {
332		let pallet_account = Pallet::<Test>::account_id();
333		let ed = <Test as Config>::Currency::minimum_balance();
334		<Test as Config>::Currency::mint_into(&pallet_account, ed).unwrap();
335		<Test as Config>::Currency::mint_into(&pallet_account, deposit).unwrap();
336		<Test as Config>::Currency::hold(
337			&HoldReason::CodeUploadDepositReserve.into(),
338			&pallet_account,
339			deposit,
340		)
341		.unwrap();
342		CodeInfoOf::<Test>::insert(hash, CodeInfo::<Test>::new_with_deposit(owner, deposit));
343	}
344
345	fn seed_contract(address: H160, code_hash: H256, storage_deposit: u128) {
346		let contract_account = <Test as Config>::AddressMapper::to_account_id(&address);
347		let info = ContractInfo::<Test>::new(&address, 0u32.into(), code_hash).unwrap();
348		AccountInfoOf::<Test>::insert(
349			address,
350			AccountInfo::<Test> { account_type: AccountType::Contract(info), dust: 0 },
351		);
352
353		let ed = <Test as Config>::Currency::minimum_balance();
354		<Test as Config>::Currency::mint_into(&contract_account, ed).unwrap();
355		<Test as Config>::Currency::mint_into(&contract_account, storage_deposit).unwrap();
356		<Test as Config>::Currency::hold(
357			&HoldReason::StorageDepositReserve.into(),
358			&contract_account,
359			storage_deposit,
360		)
361		.unwrap();
362	}
363
364	#[test]
365	fn phase_one_populates_native_deposit_for_code_upload() {
366		ExtBuilder::default().genesis_config(None).build().execute_with(|| {
367			let pallet_account = Pallet::<Test>::account_id();
368			let owner_a = AccountId32::new([1; 32]);
369			let owner_b = AccountId32::new([2; 32]);
370			seed_code_upload(H256::repeat_byte(0xAA), owner_a.clone(), 1_000);
371			seed_code_upload(H256::repeat_byte(0xAB), owner_a.clone(), 500);
372			seed_code_upload(H256::repeat_byte(0xBB), owner_b.clone(), 2_000);
373
374			V4::run_to_completion();
375
376			assert_eq!(
377				NativeDepositOf::<Test>::get(&pallet_account, &owner_a),
378				1_500,
379				"owner_a sum of code deposits"
380			);
381			assert_eq!(
382				NativeDepositOf::<Test>::get(&pallet_account, &owner_b),
383				2_000,
384				"owner_b sum of code deposits"
385			);
386
387			assert_eq!(
388				<Test as Config>::Currency::balance_on_hold(
389					&HoldReason::CodeUploadDepositReserve.into(),
390					&pallet_account,
391				),
392				3_500,
393			);
394		});
395	}
396
397	#[test]
398	fn phase_two_burns_native_and_mints_pgas_on_contracts() {
399		ExtBuilder::default().genesis_config(None).build().execute_with(|| {
400			let owner = AccountId32::new([1; 32]);
401			let hash = H256::repeat_byte(0xCC);
402			seed_code_upload(hash, owner.clone(), 0);
403
404			let c1 = H160::repeat_byte(0x10);
405			let c2 = H160::repeat_byte(0x20);
406			seed_contract(c1, hash, 700);
407			seed_contract(c2, hash, 1_300);
408
409			let c1_acc = <Test as Config>::AddressMapper::to_account_id(&c1);
410			let c2_acc = <Test as Config>::AddressMapper::to_account_id(&c2);
411
412			let total_issuance_before = <Test as Config>::Currency::total_issuance();
413
414			V4::run_to_completion();
415
416			assert_eq!(
417				<Test as Config>::Currency::balance_on_hold(
418					&HoldReason::StorageDepositReserve.into(),
419					&c1_acc,
420				),
421				0,
422			);
423			assert_eq!(
424				<Test as Config>::Currency::balance_on_hold(
425					&HoldReason::StorageDepositReserve.into(),
426					&c2_acc,
427				),
428				0,
429			);
430
431			assert_eq!(
432				total_issuance_before - <Test as Config>::Currency::total_issuance(),
433				700 + 1_300,
434			);
435
436			use frame_support::traits::tokens::fungibles::{Inspect, InspectHold};
437			let pgas_ed = Assets::minimum_balance(PGasAssetId::get());
438			assert_eq!(
439				AssetsHolder::balance_on_hold(
440					PGasAssetId::get(),
441					&HoldReason::StorageDepositReserve.into(),
442					&c1_acc,
443				),
444				700,
445			);
446			assert_eq!(
447				AssetsHolder::balance_on_hold(
448					PGasAssetId::get(),
449					&HoldReason::StorageDepositReserve.into(),
450					&c2_acc,
451				),
452				1_300,
453			);
454			// Each migrated contract also gets the PGAS ED minted into its free balance and
455			// frozen under `FreezeReason::PGasMinBalance`, matching the post-`init_contract`
456			// invariant.
457			assert_eq!(Assets::balance(PGasAssetId::get(), &c1_acc), pgas_ed);
458			assert_eq!(Assets::balance(PGasAssetId::get(), &c2_acc), pgas_ed);
459			use frame_support::traits::tokens::fungibles::InspectFreeze;
460			assert_eq!(
461				AssetsFreezer::balance_frozen(
462					PGasAssetId::get(),
463					&FreezeReason::PGasMinBalance.into(),
464					&c1_acc,
465				),
466				pgas_ed,
467			);
468			assert_eq!(
469				AssetsFreezer::balance_frozen(
470					PGasAssetId::get(),
471					&FreezeReason::PGasMinBalance.into(),
472					&c2_acc,
473				),
474				pgas_ed,
475			);
476		});
477	}
478
479	#[test]
480	fn phase_three_rewrites_legacy_deletion_queue_entries() {
481		use crate::{
482			DeletionQueueCounter,
483			storage::{DeletionQueueItem, DeletionQueueManager},
484		};
485
486		ExtBuilder::default().genesis_config(None).build().execute_with(|| {
487			let trie_a: TrieId = vec![0xAA; 16].try_into().unwrap();
488			let trie_b: TrieId = vec![0xBB; 24].try_into().unwrap();
489			old::DeletionQueue::<Test>::insert(0u32, trie_a.clone());
490			old::DeletionQueue::<Test>::insert(1u32, trie_b.clone());
491			let mut q = DeletionQueueManager::<Test>::from_test_values(2, 0);
492			DeletionQueueCounter::<Test>::set(q.clone());
493			let _ = &mut q;
494
495			V4::run_to_completion();
496
497			let zero = AccountId32::new([0u8; 32]);
498			assert_eq!(
499				DeletionQueue::<Test>::get(0u32),
500				Some(DeletionQueueItem::<Test>::new(trie_a, zero.clone())),
501			);
502			assert_eq!(
503				DeletionQueue::<Test>::get(1u32),
504				Some(DeletionQueueItem::<Test>::new(trie_b, zero)),
505			);
506		});
507	}
508
509	#[test]
510	fn eoa_accounts_are_skipped() {
511		use crate::test_utils::{ALICE, ALICE_ADDR, BOB, BOB_ADDR};
512		use frame_support::traits::tokens::fungibles::InspectHold;
513
514		ExtBuilder::default().genesis_config(None).build().execute_with(|| {
515			let _ = <Test as Config>::Currency::mint_into(&ALICE, Pallet::<Test>::min_balance());
516			let _ = <Test as Config>::Currency::mint_into(&BOB, Pallet::<Test>::min_balance());
517			AccountInfoOf::<Test>::insert(
518				ALICE_ADDR,
519				AccountInfo::<Test> { account_type: AccountType::EOA, dust: 0 },
520			);
521
522			let owner = AccountId32::new([1; 32]);
523			let hash = H256::repeat_byte(0xDD);
524			seed_code_upload(hash, owner.clone(), 0);
525			seed_contract(BOB_ADDR, hash, 400);
526
527			V4::run_to_completion();
528
529			assert_eq!(
530				AssetsHolder::balance_on_hold(
531					PGasAssetId::get(),
532					&HoldReason::StorageDepositReserve.into(),
533					&BOB,
534				),
535				400,
536			);
537		});
538	}
539}