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}