referrerpolicy=no-referrer-when-downgrade

pallet_staking_async/
migrations.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//! Storage migrations for the staking-async pallet.
19
20use crate::{
21	log, reward::EraRewardManager, Config, DisableMintingGuard, RewardKind, RewardPot,
22	WeightedPointsFormulaStartEra,
23};
24use frame_support::{
25	migrations::VersionedMigration,
26	pallet_prelude::*,
27	traits::{
28		fungible::{Inspect, Mutate},
29		tokens::Preservation,
30		Get, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade,
31	},
32	PalletId,
33};
34use sp_runtime::{traits::AccountIdConversion, Saturating};
35use sp_staking::EraIndex;
36
37/// One-shot migration relocating already-funded era pots after the seed-derivation
38/// change (#11930) so existing rewards stay claimable. For runtimes that activated
39/// DAP before the slot-based rotation of era pot accounts landed.
40///
41/// Migrates a single [`RewardKind`] per instance — list it twice in `Migrations`
42/// if both kinds need migrating.
43///
44/// Idempotent: skips eras whose old account has no balance.
45///
46/// Generic params:
47/// - `T`: pallet config.
48/// - `S`: same `Get<PalletId>` used by [`crate::Seed`] to derive pot accounts.
49/// - `K`: which [`RewardKind`] to migrate.
50pub struct MigrateEraPotsToPool<T, S, K>(core::marker::PhantomData<(T, S, K)>);
51
52impl<T: Config, S: Get<PalletId>, K: Get<RewardKind>> MigrateEraPotsToPool<T, S, K> {
53	/// Reproduces the historical seed derivation used before the slot-based
54	/// rotation, needed to locate pre-migration balances.
55	fn old_pot_account(era: EraIndex) -> T::AccountId {
56		S::get().into_sub_account_truncating(RewardPot::Era(era, K::get()))
57	}
58}
59
60impl<T: Config, S: Get<PalletId>, K: Get<RewardKind>> OnRuntimeUpgrade
61	for MigrateEraPotsToPool<T, S, K>
62{
63	fn on_runtime_upgrade() -> Weight {
64		let mut weight = T::DbWeight::get().reads(2);
65
66		let Some(guard_era) = DisableMintingGuard::<T>::get() else {
67			log!(info, "EraPotsToPool: guard unset, nothing to migrate");
68			return weight;
69		};
70
71		let active_era_idx = crate::session_rotation::Rotator::<T>::active_era();
72		debug_assert!(
73			active_era_idx >= guard_era,
74			"active_era should always be past DisableMintingGuard once set"
75		);
76		if active_era_idx <= guard_era {
77			return weight;
78		}
79
80		// Anything older than `HistoryDepth` was already cleaned up via the
81		// normal payout flow.
82		let oldest = active_era_idx.saturating_sub(T::HistoryDepth::get()).max(guard_era);
83
84		let kind = K::get();
85		let mut migrated = 0u32;
86		for era in oldest..active_era_idx {
87			let old = Self::old_pot_account(era);
88			weight.saturating_accrue(T::DbWeight::get().reads(1));
89			if frame_system::Pallet::<T>::providers(&old) == 0 {
90				continue;
91			}
92
93			// `create` is idempotent: increments the provider on the new
94			// slot account only if not already provided.
95			let new = EraRewardManager::<T>::create(era, kind);
96			weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
97
98			let balance = T::Currency::balance(&old);
99			weight.saturating_accrue(T::DbWeight::get().reads(1));
100			if !balance.is_zero() {
101				if let Err(e) = T::Currency::transfer(&old, &new, balance, Preservation::Expendable)
102				{
103					log!(
104						error,
105						"EraPotsToPool: era {} kind {:?}: transfer failed: {:?}",
106						era,
107						kind,
108						e,
109					);
110					// Keep providers on the old account; balance is still there
111					// and the account remains queryable for manual recovery.
112					continue;
113				}
114				weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
115			}
116
117			// Try to release the old drained account so it can be reaped.
118			let _ = frame_system::Pallet::<T>::dec_providers(&old);
119			weight.saturating_accrue(T::DbWeight::get().writes(1));
120			migrated.saturating_accrue(1);
121		}
122
123		log!(
124			info,
125			"EraPotsToPool: migrated {} eras of kind {:?} from guard {} to active {}",
126			migrated,
127			kind,
128			guard_era,
129			active_era_idx,
130		);
131		weight
132	}
133
134	#[cfg(feature = "try-runtime")]
135	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
136		use crate::{BalanceOf, PotAccountProvider};
137		use codec::Encode;
138		use sp_runtime::traits::Zero;
139
140		let kind = K::get();
141		let mut total_old: BalanceOf<T> = Zero::zero();
142		let mut total_new_pre: BalanceOf<T> = Zero::zero();
143		for era in Self::migrated_eras() {
144			let old = Self::old_pot_account(era);
145			total_old.saturating_accrue(T::Currency::balance(&old));
146			let new = T::RewardPots::pot_account(RewardPot::Era(era, kind));
147			total_new_pre.saturating_accrue(T::Currency::balance(&new));
148		}
149		Ok((total_old, total_new_pre).encode())
150	}
151
152	#[cfg(feature = "try-runtime")]
153	fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
154		use crate::{BalanceOf, PotAccountProvider};
155		use codec::Decode;
156		use sp_runtime::traits::Zero;
157
158		let (total_old, total_new_pre): (BalanceOf<T>, BalanceOf<T>) =
159			Decode::decode(&mut &state[..]).map_err(|_| "decode pre_upgrade state")?;
160
161		let kind = K::get();
162		let mut remaining_old: BalanceOf<T> = Zero::zero();
163		let mut total_new_post: BalanceOf<T> = Zero::zero();
164		for era in Self::migrated_eras() {
165			let old = Self::old_pot_account(era);
166			remaining_old.saturating_accrue(T::Currency::balance(&old));
167			let new = T::RewardPots::pot_account(RewardPot::Era(era, kind));
168			total_new_post.saturating_accrue(T::Currency::balance(&new));
169		}
170
171		frame_support::ensure!(
172			remaining_old.is_zero(),
173			"old pot accounts still hold balance after migration"
174		);
175		// Funds must have landed in the new pots, accounting for whatever was
176		// already there pre-migration (if anything).
177		frame_support::ensure!(
178			total_new_post.saturating_sub(total_new_pre) == total_old,
179			"new pot balances did not increase by total_old after migration"
180		);
181		Ok(())
182	}
183}
184
185#[cfg(feature = "try-runtime")]
186impl<T: Config, S: Get<PalletId>, K: Get<RewardKind>> MigrateEraPotsToPool<T, S, K> {
187	/// Returns the eras the migration touches. Only used for pre/post state checks.
188	fn migrated_eras() -> core::ops::Range<EraIndex> {
189		let active = crate::session_rotation::Rotator::<T>::active_era();
190		match DisableMintingGuard::<T>::get() {
191			Some(guard) if active > guard => {
192				let oldest = active.saturating_sub(T::HistoryDepth::get()).max(guard);
193				oldest..active
194			},
195			_ => 0..0,
196		}
197	}
198}
199
200/// Version-gated form of [`VersionUncheckedSetWeightedPointsFormulaStartEra`]
201pub type SetWeightedPointsFormulaStartEra<T> = VersionedMigration<
202	17,
203	18,
204	VersionUncheckedSetWeightedPointsFormulaStartEra<T>,
205	crate::Pallet<T>,
206	<T as frame_system::Config>::DbWeight,
207>;
208
209/// One-shot, single-block migration that records the cutoff era from which the
210/// validator self-stake incentive uses the weighted-points formula
211/// `share_i = (w_i · ep_i) / Σ_j(w_j · ep_j)`.
212///
213/// The denominator [`crate::ErasSumWeightedPoints`] is maintained incrementally by
214/// session reports as they credit reward points. Eras whose points were credited
215/// before that denominator was maintained can have a zero or incomplete value.
216/// Rather than paying the cost of recomputing it for the full
217/// [`Config::HistoryDepth`] window (`HistoryDepth × MaxValidatorSet` reads), this
218/// migration sets [`WeightedPointsFormulaStartEra`] to `active_era + 1`:
219///
220/// - eras `<= active_era` keep the legacy stake-only share `w_i / Σ_j w_j`;
221/// - eras `> active_era` use the new weighted-points share, with their
222///   [`crate::ErasSumWeightedPoints`] accumulated from session 0 of the era.
223///
224/// Chains initialized with this storage item pin the cutoff to `0` at genesis, so their
225/// already-recorded denominators continue to apply to every era.
226///
227/// Runtimes should wire the version-gated [`SetWeightedPointsFormulaStartEra`], not this type
228/// directly; it is only `pub` because the gated alias names it in its signature.
229pub struct VersionUncheckedSetWeightedPointsFormulaStartEra<T>(core::marker::PhantomData<T>);
230
231impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedSetWeightedPointsFormulaStartEra<T> {
232	fn on_runtime_upgrade() -> Weight {
233		let active_era = crate::session_rotation::Rotator::<T>::active_era();
234		// `active_era` may already have reward points credited without
235		// `ErasSumWeightedPoints` having been maintained for them, so the weighted-points formula
236		// can only safely apply from the next era onwards.
237		let cutoff = active_era.saturating_add(1);
238		WeightedPointsFormulaStartEra::<T>::put(cutoff);
239
240		log!(
241			info,
242			"WeightedPointsFormulaStartEra set to {} (active_era {} uses legacy formula)",
243			cutoff,
244			active_era,
245		);
246
247		T::DbWeight::get().reads_writes(1, 1)
248	}
249
250	#[cfg(feature = "try-runtime")]
251	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
252		use codec::Encode;
253		// Capture `active_era` before the upgrade runs so `post_upgrade` can derive the expected
254		// cutoff without re-reading it; `active_era` may otherwise differ if an era rotation occurs
255		// between the two hooks.
256		Ok(crate::session_rotation::Rotator::<T>::active_era().encode())
257	}
258
259	#[cfg(feature = "try-runtime")]
260	fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
261		use codec::Decode;
262
263		// The version gate forwards to this hook only when the migration actually ran, so the
264		// cutoff must now equal `active_era + 1`.
265		let pre_active_era = EraIndex::decode(&mut &state[..]).map_err(|_| "decode active_era")?;
266		frame_support::ensure!(
267			WeightedPointsFormulaStartEra::<T>::get() == Some(pre_active_era.saturating_add(1)),
268			"cutoff must be active_era + 1 after the migration"
269		);
270
271		Ok(())
272	}
273}