contracts_mbm/purge.rs
1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! Purges the storage of the removed `pallet-contracts`, spread over multiple blocks.
20
21use frame_support::{
22 migrations::{SteppedMigration, SteppedMigrationError},
23 storage::unhashed,
24 traits::{ConstU32, Get},
25 weights::{Weight, WeightMeter},
26 BoundedVec, ReversibleStorageHasher, Twox64Concat,
27};
28use parity_scale_codec::Decode;
29use sp_io::hashing::{blake2_256, twox_128};
30use sp_std::{marker::PhantomData, vec::Vec};
31
32use crate::{WeightInfo, LOG_TARGET};
33
34/// Upper bound on the length of a storage key handed around as a migration cursor.
35///
36/// The longest key any of these migrations sees is `twox_128(pallet) ++ twox_128(item) ++
37/// hasher(key)`, i.e. 72 bytes for `ContractInfoOf`. Measured over every live key on both
38/// mainnets; 512 leaves ~7x headroom.
39pub type MaxKeyLen = ConstU32<512>;
40
41/// Hard cap on the number of storage keys touched in a single step.
42///
43/// The weight meter is the real limit - every key costs at least a read and a write, so the meter
44/// always binds first. This is only a guard against an unbounded loop should that ever stop being
45/// true.
46pub const MAX_KEYS_PER_STEP: u32 = 10_000;
47
48/// `pallet-contracts` storage holding the `trie_id`s of contracts pending lazy deletion.
49pub const DELETION_QUEUE: &[u8] = b"DeletionQueue";
50/// `pallet-contracts` storage holding the info (starting with the `trie_id`) of live contracts.
51pub const CONTRACT_INFO_OF: &[u8] = b"ContractInfoOf";
52
53/// `pallet-contracts`' `TrieId`, i.e. `BoundedVec<u8, ConstU32<128>>`.
54pub type TrieId = BoundedVec<u8, ConstU32<128>>;
55
56/// Exact length of a `pallet-contracts` `trie_id`.
57///
58/// `ContractInfo::new` derives it from `blake2_256`, so every trie id is 32 bytes even though the
59/// type allows up to 128. Verified against all 305 Astar and 923 Shiden `ContractInfoOf` entries.
60/// Enforcing it turns a mis-parse (e.g. an upstream field reordering) into a loud, logged skip
61/// rather than a silent wrong-offset deletion.
62pub const TRIE_ID_LEN: usize = 32;
63
64/// Length of `twox_128(pallet) ++ twox_128(item)`.
65const MAP_PREFIX_LEN: usize = 32;
66
67/// Wraps a storage key into a migration cursor.
68fn to_cursor(key: Vec<u8>) -> Result<BoundedVec<u8, MaxKeyLen>, SteppedMigrationError> {
69 BoundedVec::try_from(key).map_err(|_| {
70 log::error!(
71 target: LOG_TARGET,
72 "Encountered a storage key longer than {} bytes, cannot resume 🚨",
73 <MaxKeyLen as Get<u32>>::get(),
74 );
75 SteppedMigrationError::Failed
76 })
77}
78
79/// Next storage key strictly after `from`, as long as it still lives under `prefix`.
80///
81/// Seeding `from` with the bare `prefix` yields the first key of that prefix, because `next_key`
82/// is strictly-greater and every real key is strictly longer than the prefix it lives under.
83fn next_key_under(from: &[u8], prefix: &[u8]) -> Option<Vec<u8>> {
84 sp_io::storage::next_key(from).filter(|key| key.starts_with(prefix))
85}
86
87/// Translates "ran out of weight" into the right [`SteppedMigrationError`].
88///
89/// `SteppedMigration::transactional_step` rolls back on `Err`, so `Err` is only correct when this
90/// step has not written anything yet. Conversely, a step that returns `Ok` without having removed
91/// anything would spin forever, and multi block migrations block all extrinsics while they run.
92fn not_enough_weight(
93 removed_so_far: u32,
94 required: Weight,
95) -> Result<Option<()>, SteppedMigrationError> {
96 if removed_so_far == 0 {
97 Err(SteppedMigrationError::InsufficientWeight { required })
98 } else {
99 Ok(Some(()))
100 }
101}
102
103/// Percentage of `b` that `a` represents, saturating and zero-safe.
104fn pct(a: u64, b: u64) -> u64 {
105 if b == 0 {
106 0
107 } else {
108 a.saturating_mul(100) / b
109 }
110}
111
112/// Renders a meter's whole per-block budget. Only formatted if the log level is enabled.
113struct Budget<'a>(&'a WeightMeter);
114
115impl core::fmt::Display for Budget<'_> {
116 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
117 let limit = self.0.limit();
118 write!(
119 f,
120 "{} ref_time / {} proof",
121 limit.ref_time(),
122 limit.proof_size()
123 )
124 }
125}
126
127/// Renders how much of the per-block budget a step has spent.
128///
129/// The proof percentage is the number to watch on a dry run: it is what the placeholder weights in
130/// `weights.rs` are guessing at, and what keeps a block from going PoV-oversized.
131struct Spent<'a>(&'a WeightMeter);
132
133impl core::fmt::Display for Spent<'_> {
134 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
135 let (used, limit) = (self.0.consumed(), self.0.limit());
136 write!(
137 f,
138 "spent {}/{} ref_time ({}%), {}/{} proof ({}%)",
139 used.ref_time(),
140 limit.ref_time(),
141 pct(used.ref_time(), limit.ref_time()),
142 used.proof_size(),
143 limit.proof_size(),
144 pct(used.proof_size(), limit.proof_size()),
145 )
146 }
147}
148
149/// Human readable name of a `pallet-contracts` storage item, for logging.
150fn source_name(item: &[u8]) -> &str {
151 sp_std::str::from_utf8(item).unwrap_or("<non-utf8>")
152}
153
154/// Per-step tally, logged on every exit path so a dry run can be followed block by block.
155#[derive(Default)]
156struct StepStats {
157 /// Top level `DeletionQueue` / `ContractInfoOf` entries dropped.
158 entries: u32,
159 /// Child trie keys removed.
160 child_keys: u32,
161 /// Consumer references handed back to contract accounts.
162 consumers: u32,
163}
164
165impl StepStats {
166 /// Every key this step actually removed - what the budget and the rollback rule count.
167 fn removed(&self) -> u32 {
168 self.entries.saturating_add(self.child_keys)
169 }
170}
171
172impl core::fmt::Display for StepStats {
173 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
174 write!(
175 f,
176 "{} entries, {} child keys, {} consumer refs returned",
177 self.entries, self.child_keys, self.consumers
178 )
179 }
180}
181
182/// Outcome of a bounded removal loop.
183pub(crate) enum Progress {
184 /// Nothing is left to remove.
185 Finished,
186 /// The loop stopped early. `required` carries the weight of the key that could not be paid
187 /// for, and is `None` when the loop stopped because it hit its key budget instead.
188 Exhausted { required: Option<Weight> },
189}
190
191/// Multi block variant of `frame_support::migrations::RemovePallet`.
192///
193/// Removes every storage key under a pallet's prefix, spread over as many blocks as
194/// `pallet-migrations` needs. `RemovePallet` itself is unusable here: it wipes the whole prefix
195/// within a single block.
196///
197/// The cursor carries the last removed key so that iteration always resumes where it left off.
198/// Restarting from the bare prefix instead would be quadratic: `next_key` has to walk over every
199/// deleted-but-not-yet-committed key sitting in the block's storage overlay.
200pub struct RemovePalletStepped<P, W>(PhantomData<(P, W)>);
201
202impl<P: Get<&'static str>, W> RemovePalletStepped<P, W> {
203 /// Hashed storage prefix of the pallet that is being removed.
204 fn hashed_prefix() -> [u8; 16] {
205 twox_128(P::get().as_bytes())
206 }
207}
208
209impl<P, W> SteppedMigration for RemovePalletStepped<P, W>
210where
211 P: Get<&'static str>,
212 W: WeightInfo,
213{
214 /// Last removed key. `None` means "start from the beginning of the prefix".
215 type Cursor = BoundedVec<u8, MaxKeyLen>;
216 /// Derived from the pallet name so that two instances never collide.
217 type Identifier = [u8; 32];
218
219 fn id() -> Self::Identifier {
220 blake2_256(
221 &[
222 b"contracts-mbm::RemovePalletStepped::".as_slice(),
223 P::get().as_bytes(),
224 ]
225 .concat(),
226 )
227 }
228
229 fn step(
230 cursor: Option<Self::Cursor>,
231 meter: &mut WeightMeter,
232 ) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
233 let prefix = Self::hashed_prefix();
234 // The only bit the cursor carries: `None` means this is the first step of the migration.
235 if cursor.is_none() {
236 log::info!(
237 target: LOG_TARGET,
238 "remove<{}>: starting, budget {}",
239 P::get(),
240 Budget(meter),
241 );
242 }
243 let mut from = cursor
244 .map(BoundedVec::into_inner)
245 .unwrap_or_else(|| prefix.to_vec());
246 let mut removed = 0u32;
247 let mut bytes = 0u64;
248
249 loop {
250 let Some(key) = next_key_under(&from, &prefix) else {
251 log::info!(
252 target: LOG_TARGET,
253 "remove<{}>: DONE ✅ prefix empty | this step: {removed} keys, {bytes} value \
254 bytes | {}",
255 P::get(),
256 Spent(meter),
257 );
258 return Ok(None);
259 };
260
261 if removed >= MAX_KEYS_PER_STEP {
262 log::info!(
263 target: LOG_TARGET,
264 "remove<{}>: paused on the {MAX_KEYS_PER_STEP} key/step cap | this step: \
265 {removed} keys, {bytes} value bytes | {}",
266 P::get(),
267 Spent(meter),
268 );
269 return to_cursor(from).map(Some);
270 }
271
272 // Measure the value without copying it into the runtime; it enters the PoV either way.
273 let value_len = sp_io::storage::read(&key, &mut [], 0).unwrap_or_default();
274 let cost = W::remove_key(value_len);
275
276 if meter.try_consume(cost).is_err() {
277 return if removed == 0 {
278 log::warn!(
279 target: LOG_TARGET,
280 "remove<{}>: cannot afford a single key - needs {} ref_time / {} proof but \
281 the whole block budget is {} 🚨",
282 P::get(),
283 cost.ref_time(),
284 cost.proof_size(),
285 Budget(meter),
286 );
287 Err(SteppedMigrationError::InsufficientWeight { required: cost })
288 } else {
289 log::info!(
290 target: LOG_TARGET,
291 "remove<{}>: out of weight, resuming next block | this step: {removed} \
292 keys, {bytes} value bytes | {}",
293 P::get(),
294 Spent(meter),
295 );
296 to_cursor(from).map(Some)
297 };
298 }
299
300 sp_io::storage::clear(&key);
301 removed = removed.saturating_add(1);
302 bytes = bytes.saturating_add(value_len as u64);
303 from = key;
304 }
305 }
306
307 #[cfg(feature = "try-runtime")]
308 fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
309 let prefix = Self::hashed_prefix();
310 let (mut keys, mut bytes) = (0u32, 0u64);
311 let mut from = prefix.to_vec();
312 while let Some(key) = next_key_under(&from, &prefix) {
313 keys = keys.saturating_add(1);
314 bytes = bytes
315 .saturating_add(sp_io::storage::read(&key, &mut [], 0).unwrap_or_default() as u64);
316 from = key;
317 }
318
319 log::info!(
320 target: LOG_TARGET,
321 "remove<{}>: pre_upgrade sees {keys} keys holding {bytes} value bytes 👀",
322 P::get(),
323 );
324 Ok(Vec::new())
325 }
326
327 #[cfg(feature = "try-runtime")]
328 fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
329 use frame_support::storage::unhashed::contains_prefixed_key;
330
331 if contains_prefixed_key(&Self::hashed_prefix()) {
332 return Err("Keys remaining post-removal, this should never happen 🚨".into());
333 }
334
335 log::info!(
336 target: LOG_TARGET,
337 "remove<{}>: post_upgrade OK, prefix is empty ✅",
338 P::get(),
339 );
340 Ok(())
341 }
342}
343
344/// Purges the child tries owned by the decommissioned `pallet-contracts`, and hands back the
345/// consumer references it took on contract accounts.
346///
347/// `pallet-contracts` keeps the storage of every instantiated contract in a *child trie* addressed
348/// by that contract's `trie_id`, and holds only a pointer to it in its own (top level) storage.
349/// Wiping the pallet prefix therefore does **not** free the bulk of the data, it merely orphans it:
350/// the `trie_id`s needed to address those child tries would be gone for good.
351///
352/// This migration walks the two places where `trie_id`s are recorded and empties the corresponding
353/// child tries before dropping the top level entry pointing at them:
354/// * `Contracts::DeletionQueue` - tries of already terminated contracts, awaiting lazy deletion.
355/// * `Contracts::ContractInfoOf` - tries of contracts that are still alive.
356///
357/// It MUST be scheduled *before* [`RemovePalletStepped`] for the same pallet, otherwise the
358/// `trie_id`s are deleted first and the child tries become unreachable forever.
359///
360/// # Consumer references
361///
362/// `pallet-contracts` calls `inc_consumers` on the contract account at instantiation and only
363/// gives that reference back from `seal_terminate`. `ContractInfoOf` is the last on-chain record
364/// of which accounts are contracts, so purging it is the final opportunity to hand the reference
365/// back - leave it and every live contract account keeps a dangling consumer forever, and can
366/// never be reaped even once its balance is gone. `DeletionQueue` entries are terminated
367/// contracts, which already gave theirs back, hence the distinction below.
368///
369/// # Storage layout coupling
370///
371/// Values are decoded structurally rather than via `pallet-contracts` types, since the pallet is
372/// no longer a dependency of the runtimes. Verified against `polkadot-sdk` `stable2512`:
373/// * `DeletionQueue: StorageMap<_, Twox64Concat, u32, TrieId>` - the value *is* a `TrieId`.
374/// * `ContractInfoOf: StorageMap<_, Twox64Concat, AccountId, ContractInfo<T>>`, and
375/// `ContractInfo`'s **first** field is `pub trie_id: TrieId`, so it decodes off the front.
376///
377/// A `TrieId` is a length prefixed byte blob; decoding it as a [`TrieId`] (rather than a plain
378/// `Vec<u8>`) makes a garbage value fail loudly instead of producing a bogus multi kilobyte id.
379pub struct PurgeContractsChildTries<T, P, W>(PhantomData<(T, P, W)>);
380
381impl<T: frame_system::Config, P: Get<&'static str>, W> PurgeContractsChildTries<T, P, W> {
382 /// Hashed prefix of a `pallet-contracts` storage map.
383 pub(crate) fn map_prefix(item: &[u8]) -> Vec<u8> {
384 [twox_128(P::get().as_bytes()), twox_128(item)].concat()
385 }
386
387 /// The storage maps holding `trie_id`s, paired with whether their key is a live contract
388 /// account still owing a consumer reference. Terminated contracts come first, they are dead
389 /// weight.
390 fn trie_id_sources() -> [(&'static [u8], bool); 2] {
391 [(DELETION_QUEUE, false), (CONTRACT_INFO_OF, true)]
392 }
393
394 /// Gives back the consumer reference `pallet-contracts` took on the contract account keyed by
395 /// `ContractInfoOf` key `key`.
396 pub(crate) fn release_consumer(key: &[u8]) -> bool {
397 let account = key
398 .get(MAP_PREFIX_LEN..)
399 .map(Twox64Concat::reverse)
400 .and_then(|mut raw| T::AccountId::decode(&mut raw).ok());
401
402 let Some(account) = account else {
403 log::warn!(
404 target: LOG_TARGET,
405 "purge: no account id in a ContractInfoOf key, consumer ref kept 🚨",
406 );
407 return false;
408 };
409
410 // Guarded rather than unconditional: `dec_consumers` on an account that has none is a
411 // logged logic error, and it would write back a default `AccountInfo` for an account that
412 // no longer exists.
413 let before = frame_system::Pallet::<T>::consumers(&account);
414 if before == 0 {
415 log::debug!(
416 target: LOG_TARGET,
417 "purge: {account:?} is already at 0 consumers, nothing to hand back",
418 );
419 return false;
420 }
421
422 frame_system::Pallet::<T>::dec_consumers(&account);
423 log::debug!(
424 target: LOG_TARGET,
425 "purge: {account:?} consumers {before} -> {}",
426 before.saturating_sub(1),
427 );
428 true
429 }
430}
431
432impl<T, P, W> SteppedMigration for PurgeContractsChildTries<T, P, W>
433where
434 T: frame_system::Config,
435 P: Get<&'static str>,
436 W: WeightInfo,
437{
438 /// Progress lives in storage itself: every step restarts from the first remaining entry, and
439 /// an entry is only dropped once its child trie has been fully emptied. Restarting is cheap
440 /// because the number of contracts is small - the child tries themselves, which are not, are
441 /// walked with a local cursor.
442 type Cursor = ();
443 /// Derived from the pallet name so that two instances never collide.
444 type Identifier = [u8; 32];
445
446 fn id() -> Self::Identifier {
447 blake2_256(
448 &[
449 b"contracts-mbm::PurgeContractsChildTries::".as_slice(),
450 P::get().as_bytes(),
451 ]
452 .concat(),
453 )
454 }
455
456 fn step(
457 cursor: Option<Self::Cursor>,
458 meter: &mut WeightMeter,
459 ) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
460 // The only bit the cursor carries: `None` means this is the first step of the migration.
461 // Progress itself lives in storage - an entry is dropped only once its child trie is
462 // empty, so a restart resumes at the first entry that is still there.
463 if cursor.is_none() {
464 log::info!(
465 target: LOG_TARGET,
466 "purge: starting, budget {}",
467 Budget(meter),
468 );
469 }
470
471 // Counts every key this step actually removed, top level entries and child trie keys
472 // alike. Used to decide between `Err(InsufficientWeight)` (rolls back, so only valid when
473 // nothing was written) and `Ok(Some(()))` (commits and resumes).
474 let mut stats = StepStats::default();
475
476 for (item, live) in Self::trie_id_sources() {
477 let name = source_name(item);
478 let prefix = Self::map_prefix(item);
479 let mut from = prefix.clone();
480 let entries_before = stats.entries;
481
482 while let Some(key) = next_key_under(&from, &prefix) {
483 if stats.removed() >= MAX_KEYS_PER_STEP {
484 log::info!(
485 target: LOG_TARGET,
486 "purge: paused on the {MAX_KEYS_PER_STEP} key/step cap in {name} | this \
487 step: {stats} | {}",
488 Spent(meter),
489 );
490 return Ok(Some(()));
491 }
492
493 // The whole value must be pulled into the PoV to get at the `trie_id` anyway.
494 let raw = unhashed::get_raw(&key);
495 let mut cost =
496 W::remove_key(raw.as_ref().map(|v| v.len() as u32).unwrap_or_default());
497 if live {
498 cost = cost.saturating_add(W::release_contract_consumer());
499 }
500 if meter.try_consume(cost).is_err() {
501 log::info!(
502 target: LOG_TARGET,
503 "purge: out of weight before a {name} entry | this step: {stats} | {}",
504 Spent(meter),
505 );
506 return not_enough_weight(stats.removed(), cost);
507 }
508
509 let maybe_trie_id = raw
510 .and_then(|raw| TrieId::decode(&mut &raw[..]).ok())
511 .filter(|trie_id| trie_id.len() == TRIE_ID_LEN);
512
513 if let Some(trie_id) = maybe_trie_id {
514 let budget = MAX_KEYS_PER_STEP.saturating_sub(stats.removed());
515 let (progress, child_keys_removed) =
516 clear_child_trie_metered::<W>(&trie_id, meter, budget);
517 stats.child_keys = stats.child_keys.saturating_add(child_keys_removed);
518
519 if let Progress::Exhausted { required } = progress {
520 // Keep the top level entry so the next step picks the same trie up again.
521 log::info!(
522 target: LOG_TARGET,
523 "purge: child trie only partly emptied ({child_keys_removed} keys \
524 this step), its {name} entry is kept and will be resumed | this \
525 step: {stats} | {}",
526 Spent(meter),
527 );
528 return match required {
529 Some(cost) => not_enough_weight(stats.removed(), cost),
530 None => Ok(Some(())),
531 };
532 }
533
534 log::debug!(
535 target: LOG_TARGET,
536 "purge: {name} entry cleared, {child_keys_removed} child trie keys removed",
537 );
538 } else {
539 // Either the value is too short, or what sits at the front is not a 32 byte
540 // blob - in both cases there is nothing addressable behind it. Dropping the
541 // entry is safe; the loud warning is what matters, because a systematic
542 // occurrence would mean the assumed `ContractInfo` layout is wrong.
543 log::warn!(
544 target: LOG_TARGET,
545 "purge: no {TRIE_ID_LEN}-byte trie id at the front of a {name} entry, \
546 dropping it without touching any child trie 🚨",
547 );
548 }
549
550 if live && Self::release_consumer(&key) {
551 stats.consumers = stats.consumers.saturating_add(1);
552 }
553 unhashed::kill(&key);
554 stats.entries = stats.entries.saturating_add(1);
555 from = key;
556 }
557
558 // Only worth saying when this step is the one that finished the map off; the scan
559 // restarts from the top every step, so an already-empty map falls through here too.
560 if stats.entries > entries_before {
561 log::info!(
562 target: LOG_TARGET,
563 "purge: {name} is now empty, {} entries dropped this step",
564 stats.entries.saturating_sub(entries_before),
565 );
566 }
567 }
568
569 log::info!(
570 target: LOG_TARGET,
571 "purge: DONE ✅ every contract child trie purged | this step: {stats} | {}",
572 Spent(meter),
573 );
574 Ok(None)
575 }
576
577 #[cfg(feature = "try-runtime")]
578 fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
579 use parity_scale_codec::Encode;
580
581 // Snapshot every trie id so `post_upgrade` can prove the child tries are really gone -
582 // once the top level entries are removed they would be unreachable and unverifiable.
583 let mut trie_ids: Vec<Vec<u8>> = Vec::new();
584 let mut consumers_owed = 0u32;
585
586 for (item, live) in Self::trie_id_sources() {
587 let name = source_name(item);
588 let prefix = Self::map_prefix(item);
589 let mut from = prefix.clone();
590 let (mut entries, mut undecodable) = (0u32, 0u32);
591
592 while let Some(key) = next_key_under(&from, &prefix) {
593 entries = entries.saturating_add(1);
594 match unhashed::get_raw(&key)
595 .and_then(|raw| TrieId::decode(&mut &raw[..]).ok())
596 .filter(|trie_id| trie_id.len() == TRIE_ID_LEN)
597 {
598 Some(trie_id) => trie_ids.push(trie_id.into_inner()),
599 None => undecodable = undecodable.saturating_add(1),
600 }
601
602 if live {
603 // How many accounts will actually get a reference back. Anything short of
604 // `entries` means some contract accounts are not carrying the ref this
605 // migration assumes - worth knowing before enactment, not after.
606 let has_ref = key
607 .get(MAP_PREFIX_LEN..)
608 .map(Twox64Concat::reverse)
609 .and_then(|mut raw| T::AccountId::decode(&mut raw).ok())
610 .is_some_and(|who| frame_system::Pallet::<T>::consumers(&who) > 0);
611 if has_ref {
612 consumers_owed = consumers_owed.saturating_add(1);
613 }
614 }
615 from = key;
616 }
617
618 log::info!(
619 target: LOG_TARGET,
620 "purge: pre_upgrade sees {entries} {name} entries ({undecodable} without a usable \
621 trie id) 👀",
622 );
623 }
624
625 log::info!(
626 target: LOG_TARGET,
627 "purge: pre_upgrade snapshotted {} child tries to empty, and {consumers_owed} contract \
628 accounts are owed a consumer ref 👀",
629 trie_ids.len(),
630 );
631
632 Ok(trie_ids.encode())
633 }
634
635 #[cfg(feature = "try-runtime")]
636 fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
637 use frame_support::storage::unhashed::contains_prefixed_key;
638
639 for (item, _) in Self::trie_id_sources() {
640 if contains_prefixed_key(&Self::map_prefix(item)) {
641 return Err("Contract trie id entries remaining post-removal 🚨".into());
642 }
643 }
644
645 let trie_ids = Vec::<Vec<u8>>::decode(&mut &state[..])
646 .map_err(|_| "Failed to decode the pre-upgrade trie id snapshot")?;
647 let checked = trie_ids.len();
648 for trie_id in trie_ids {
649 if sp_io::default_child_storage::next_key(&trie_id, &[]).is_some() {
650 return Err("An orphaned contract child trie is still populated 🚨".into());
651 }
652 }
653
654 log::info!(
655 target: LOG_TARGET,
656 "purge: post_upgrade OK, {checked} child tries verified empty and no trie id entries \
657 remain ✅",
658 );
659 Ok(())
660 }
661}
662
663/// Removes keys of the default child trie `trie_id` until the meter or `max_keys` is exhausted.
664///
665/// Returns how many keys were removed alongside the progress made. Iteration uses a local cursor,
666/// so the cost stays linear even when a single trie spans several steps within one block.
667pub(crate) fn clear_child_trie_metered<W: WeightInfo>(
668 trie_id: &[u8],
669 meter: &mut WeightMeter,
670 max_keys: u32,
671) -> (Progress, u32) {
672 // The empty key sorts before every real key, so it yields the first entry of the trie.
673 let mut from: Vec<u8> = Vec::new();
674 let mut removed = 0u32;
675
676 while let Some(key) = sp_io::default_child_storage::next_key(trie_id, &from) {
677 if removed >= max_keys {
678 return (Progress::Exhausted { required: None }, removed);
679 }
680
681 let value_len =
682 sp_io::default_child_storage::read(trie_id, &key, &mut [], 0).unwrap_or_default();
683 let cost = W::remove_child_key(value_len);
684
685 if meter.try_consume(cost).is_err() {
686 return (
687 Progress::Exhausted {
688 required: Some(cost),
689 },
690 removed,
691 );
692 }
693
694 sp_io::default_child_storage::clear(trie_id, &key);
695 removed = removed.saturating_add(1);
696 from = key;
697 }
698
699 (Progress::Finished, removed)
700}