pallet_fast_unstake/lib.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//! > Made with *Substrate*, for *Polkadot*.
19//!
20//! [![github]](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/fast-unstake) -
21//! [![polkadot]](https://polkadot.com)
22//!
23//! [polkadot]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white
24//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
25//!
26//! # Fast Unstake Pallet
27//!
28//! A pallet to allow participants of the staking system (represented by [`Config::Staking`], being
29//! [`StakingInterface`]) to unstake quicker, if and only if they meet the condition of not being
30//! exposed to any slashes.
31//!
32//! ## Overview
33//!
34//! If a nominator is not exposed anywhere in the staking system, checked via
35//! [`StakingInterface::is_exposed_in_era`] (i.e. "has not actively backed any validators in the
36//! last [`StakingInterface::bonding_duration`] days"), then they can register themselves in this
37//! pallet and unstake faster than having to wait an entire bonding duration.
38//!
39//! *Being exposed with validator* from the point of view of the staking system means earning
40//! rewards with the validator, and also being at the risk of slashing with the validator. This is
41//! equivalent to the "Active Nominator" role explained in
42//! [here](https://polkadot.com/blog/staking-update-february-2022/).
43//!
44//! Stakers who are certain about NOT being exposed can register themselves with
45//! [`Pallet::register_fast_unstake`]. This will chill, fully unbond the staker and place them
46//! in the queue to be checked.
47//!
48//! A successful registration implies being fully unbonded and chilled in the staking system. These
49//! effects persist even if the fast-unstake registration is retracted (see [`Pallet::deregister`]
50//! and further).
51//!
52//! Once registered as a fast-unstaker, the staker will be queued and checked by the system. This
53//! can take a variable number of blocks based on demand, but will almost certainly be "faster" (as
54//! the name suggest) than waiting the standard bonding duration.
55//!
56//! A fast-unstaker is either in [`Queue`] or actively being checked, at which point it lives in
57//! [`Head`]. Once in [`Head`], the request cannot be retracted anymore. But, once in [`Queue`], it
58//! can, via [`Pallet::deregister`].
59//!
60//! A deposit equal to [`Config::Deposit`] is collected for this process, and is returned in case a
61//! successful unstake occurs (`Event::Unstaked` signals that).
62//!
63//! Once processed, if successful, no additional fee for the checking process is taken, and the
64//! staker is instantly unbonded.
65//!
66//! If unsuccessful, meaning that the staker was exposed, the aforementioned deposit will be slashed
67//! for the amount of wasted work they have inflicted on the chain.
68//!
69//! All in all, this pallet is meant to provide an easy off-ramp for some stakers.
70//!
71//! ### Example
72//!
73//! 1. Fast-unstake with multiple participants in the queue.
74#![doc = docify::embed!("src/tests.rs", successful_multi_queue)]
75//!
76//! 2. Fast unstake failing because a nominator is exposed.
77#![doc = docify::embed!("src/tests.rs", exposed_nominator_cannot_unstake)]
78//!
79//! ## Pallet API
80//!
81//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
82//! including its configuration trait, dispatchables, storage items, events and errors.
83//!
84//! ## Low Level / Implementation Details
85//!
86//! This pallet works off the basis of `on_idle`, meaning that it provides no guarantee about when
87//! it will succeed, if at all. Moreover, the queue implementation is unordered. In case of
88//! congestion, no FIFO ordering is provided.
89//!
90//! A few important considerations can be concluded based on the `on_idle`-based implementation:
91//!
92//! * It is crucial for the weights of this pallet to be correct. The code inside
93//! [`Pallet::on_idle`] MUST be able to measure itself and report the remaining weight correctly
94//! after execution.
95//!
96//! * If the weight measurement is incorrect, it can lead to perpetual overweight (consequently
97//! slow) blocks.
98//!
99//! * The amount of weight that `on_idle` consumes is a direct function of [`ErasToCheckPerBlock`].
100//!
101//! * Thus, a correct value of [`ErasToCheckPerBlock`] (which can be set via [`Pallet::control`])
102//! should be chosen, such that a reasonable amount of weight is used `on_idle`. If
103//! [`ErasToCheckPerBlock`] is too large, `on_idle` will always conclude that it has not enough
104//! weight to proceed, and will early-return. Nonetheless, this should also be *safe* as long as
105//! the benchmarking/weights are *accurate*.
106//!
107//! * See the inline code-comments on `do_on_idle` (private) for more details.
108//!
109//! * For further safety, in case of any unforeseen errors, the pallet will emit
110//! [`Event::InternalError`] and set [`ErasToCheckPerBlock`] back to 0, which essentially means
111//! the pallet will halt/disable itself.
112
113#![cfg_attr(not(feature = "std"), no_std)]
114
115extern crate alloc;
116
117pub use pallet::*;
118
119#[cfg(test)]
120mod mock;
121
122#[cfg(test)]
123mod tests;
124
125// NOTE: enable benchmarking in tests as well.
126#[cfg(feature = "runtime-benchmarks")]
127mod benchmarking;
128pub mod migrations;
129pub mod types;
130pub mod weights;
131
132// some extra imports for docs to link properly.
133#[cfg(doc)]
134pub use frame_support::traits::Hooks;
135#[cfg(doc)]
136pub use sp_staking::StakingInterface;
137
138/// The logging target of this pallet.
139pub const LOG_TARGET: &'static str = "runtime::fast-unstake";
140
141#[macro_export]
142macro_rules! log {
143 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
144 log::$level!(
145 target: crate::LOG_TARGET,
146 concat!("[{:?}] 💨 ", $patter), frame_system::Pallet::<T>::block_number() $(, $values)*
147 )
148 };
149}
150
151#[frame_support::pallet]
152pub mod pallet {
153 use super::*;
154 use crate::types::*;
155 use alloc::vec::Vec;
156 use frame_support::{
157 pallet_prelude::*,
158 traits::{Defensive, ReservableCurrency, StorageVersion},
159 };
160 use frame_system::pallet_prelude::*;
161 use sp_runtime::{traits::Zero, DispatchResult};
162 use sp_staking::{EraIndex, StakingInterface};
163 pub use weights::WeightInfo;
164
165 #[cfg(feature = "try-runtime")]
166 use sp_runtime::TryRuntimeError;
167
168 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
169
170 #[pallet::pallet]
171 #[pallet::storage_version(STORAGE_VERSION)]
172 pub struct Pallet<T>(_);
173
174 #[pallet::config]
175 pub trait Config: frame_system::Config {
176 /// The overarching event type.
177 #[allow(deprecated)]
178 type RuntimeEvent: From<Event<Self>>
179 + IsType<<Self as frame_system::Config>::RuntimeEvent>
180 + TryInto<Event<Self>>;
181
182 /// The currency used for deposits.
183 type Currency: ReservableCurrency<Self::AccountId>;
184
185 /// Deposit to take for unstaking, to make sure we're able to slash the it in order to cover
186 /// the costs of resources on unsuccessful unstake.
187 #[pallet::constant]
188 type Deposit: Get<BalanceOf<Self>>;
189
190 /// The origin that can control this pallet, in other words invoke [`Pallet::control`].
191 type ControlOrigin: frame_support::traits::EnsureOrigin<Self::RuntimeOrigin>;
192
193 /// Batch size.
194 ///
195 /// This many stashes are processed in each unstake request.
196 type BatchSize: Get<u32>;
197
198 /// The access to staking functionality.
199 type Staking: StakingInterface<Balance = BalanceOf<Self>, AccountId = Self::AccountId>;
200
201 /// Maximum value for `ErasToCheckPerBlock`, checked in [`Pallet::control`].
202 ///
203 /// This should be slightly bigger than the actual value in order to have accurate
204 /// benchmarks.
205 type MaxErasToCheckPerBlock: Get<u32>;
206
207 /// The weight information of this pallet.
208 type WeightInfo: WeightInfo;
209 }
210
211 /// The current "head of the queue" being unstaked.
212 ///
213 /// The head in itself can be a batch of up to [`Config::BatchSize`] stakers.
214 #[pallet::storage]
215 pub type Head<T: Config> = StorageValue<_, UnstakeRequest<T>, OptionQuery>;
216
217 /// The map of all accounts wishing to be unstaked.
218 ///
219 /// Keeps track of `AccountId` wishing to unstake and it's corresponding deposit.
220 // Hasher: Twox safe since `AccountId` is a secure hash.
221 #[pallet::storage]
222 pub type Queue<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, BalanceOf<T>>;
223
224 /// Number of eras to check per block.
225 ///
226 /// If set to 0, this pallet does absolutely nothing. Cannot be set to more than
227 /// [`Config::MaxErasToCheckPerBlock`].
228 ///
229 /// Based on the amount of weight available at [`Pallet::on_idle`], up to this many eras are
230 /// checked. The checking is represented by updating [`UnstakeRequest::checked`], which is
231 /// stored in [`Head`].
232 #[pallet::storage]
233 pub type ErasToCheckPerBlock<T: Config> = StorageValue<_, u32, ValueQuery>;
234
235 #[pallet::event]
236 #[pallet::generate_deposit(pub(super) fn deposit_event)]
237 pub enum Event<T: Config> {
238 /// A staker was unstaked.
239 Unstaked { stash: T::AccountId, result: DispatchResult },
240 /// A staker was slashed for requesting fast-unstake whilst being exposed.
241 Slashed { stash: T::AccountId, amount: BalanceOf<T> },
242 /// A batch was partially checked for the given eras, but the process did not finish.
243 BatchChecked { eras: Vec<EraIndex> },
244 /// A batch of a given size was terminated.
245 ///
246 /// This is always follows by a number of `Unstaked` or `Slashed` events, marking the end
247 /// of the batch. A new batch will be created upon next block.
248 BatchFinished { size: u32 },
249 /// An internal error happened. Operations will be paused now.
250 InternalError,
251 }
252
253 #[pallet::error]
254 #[cfg_attr(test, derive(PartialEq))]
255 pub enum Error<T> {
256 /// The provided Controller account was not found.
257 ///
258 /// This means that the given account is not bonded.
259 NotController,
260 /// The bonded account has already been queued.
261 AlreadyQueued,
262 /// The bonded account has active unlocking chunks.
263 NotFullyBonded,
264 /// The provided un-staker is not in the `Queue`.
265 NotQueued,
266 /// The provided un-staker is already in Head, and cannot deregister.
267 AlreadyHead,
268 /// The call is not allowed at this point because the pallet is not active.
269 CallNotAllowed,
270 }
271
272 #[pallet::hooks]
273 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
274 fn on_idle(_: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
275 if remaining_weight.any_lt(T::DbWeight::get().reads(2)) {
276 return Weight::from_parts(0, 0)
277 }
278
279 Self::do_on_idle(remaining_weight)
280 }
281
282 fn integrity_test() {
283 // Ensure that the value of `ErasToCheckPerBlock` is less or equal to
284 // `T::MaxErasToCheckPerBlock`.
285 assert!(
286 ErasToCheckPerBlock::<T>::get() <= T::MaxErasToCheckPerBlock::get(),
287 "the value of `ErasToCheckPerBlock` is greater than `T::MaxErasToCheckPerBlock`",
288 );
289 }
290
291 #[cfg(feature = "try-runtime")]
292 fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
293 // ensure that the value of `ErasToCheckPerBlock` is less than
294 // `T::MaxErasToCheckPerBlock`.
295 ensure!(
296 ErasToCheckPerBlock::<T>::get() <= T::MaxErasToCheckPerBlock::get(),
297 "the value of `ErasToCheckPerBlock` is greater than `T::MaxErasToCheckPerBlock`",
298 );
299
300 Ok(())
301 }
302 }
303
304 #[pallet::call]
305 impl<T: Config> Pallet<T> {
306 /// Register oneself for fast-unstake.
307 ///
308 /// ## Dispatch Origin
309 ///
310 /// The dispatch origin of this call must be *signed* by whoever is permitted to call
311 /// unbond funds by the staking system. See [`Config::Staking`].
312 ///
313 /// ## Details
314 ///
315 /// The stash associated with the origin must have no ongoing unlocking chunks. If
316 /// successful, this will fully unbond and chill the stash. Then, it will enqueue the stash
317 /// to be checked in further blocks.
318 ///
319 /// If by the time this is called, the stash is actually eligible for fast-unstake, then
320 /// they are guaranteed to remain eligible, because the call will chill them as well.
321 ///
322 /// If the check works, the entire staking data is removed, i.e. the stash is fully
323 /// unstaked.
324 ///
325 /// If the check fails, the stash remains chilled and waiting for being unbonded as in with
326 /// the normal staking system, but they lose part of their unbonding chunks due to consuming
327 /// the chain's resources.
328 ///
329 /// ## Events
330 ///
331 /// Some events from the staking and currency system might be emitted.
332 #[pallet::call_index(0)]
333 #[pallet::weight(<T as Config>::WeightInfo::register_fast_unstake())]
334 pub fn register_fast_unstake(origin: OriginFor<T>) -> DispatchResult {
335 let ctrl = ensure_signed(origin)?;
336
337 ensure!(ErasToCheckPerBlock::<T>::get() != 0, Error::<T>::CallNotAllowed);
338 let stash_account =
339 T::Staking::stash_by_ctrl(&ctrl).map_err(|_| Error::<T>::NotController)?;
340 ensure!(!Queue::<T>::contains_key(&stash_account), Error::<T>::AlreadyQueued);
341 ensure!(!Self::is_head(&stash_account), Error::<T>::AlreadyHead);
342 ensure!(!T::Staking::is_unbonding(&stash_account)?, Error::<T>::NotFullyBonded);
343
344 // chill and fully unstake.
345 T::Staking::chill(&stash_account)?;
346 T::Staking::fully_unbond(&stash_account)?;
347
348 T::Currency::reserve(&stash_account, T::Deposit::get())?;
349
350 // enqueue them.
351 Queue::<T>::insert(stash_account, T::Deposit::get());
352 Ok(())
353 }
354
355 /// Deregister oneself from the fast-unstake.
356 ///
357 /// ## Dispatch Origin
358 ///
359 /// The dispatch origin of this call must be *signed* by whoever is permitted to call
360 /// unbond funds by the staking system. See [`Config::Staking`].
361 ///
362 /// ## Details
363 ///
364 /// This is useful if one is registered, they are still waiting, and they change their mind.
365 ///
366 /// Note that the associated stash is still fully unbonded and chilled as a consequence of
367 /// calling [`Pallet::register_fast_unstake`]. Therefore, this should probably be followed
368 /// by a call to `rebond` in the staking system.
369 ///
370 /// ## Events
371 ///
372 /// Some events from the staking and currency system might be emitted.
373 #[pallet::call_index(1)]
374 #[pallet::weight(<T as Config>::WeightInfo::deregister())]
375 pub fn deregister(origin: OriginFor<T>) -> DispatchResult {
376 let ctrl = ensure_signed(origin)?;
377
378 ensure!(ErasToCheckPerBlock::<T>::get() != 0, Error::<T>::CallNotAllowed);
379
380 let stash_account =
381 T::Staking::stash_by_ctrl(&ctrl).map_err(|_| Error::<T>::NotController)?;
382 ensure!(Queue::<T>::contains_key(&stash_account), Error::<T>::NotQueued);
383 ensure!(!Self::is_head(&stash_account), Error::<T>::AlreadyHead);
384 let deposit = Queue::<T>::take(stash_account.clone());
385
386 if let Some(deposit) = deposit.defensive() {
387 let remaining = T::Currency::unreserve(&stash_account, deposit);
388 if !remaining.is_zero() {
389 Self::halt("not enough balance to unreserve");
390 }
391 }
392
393 Ok(())
394 }
395
396 /// Control the operation of this pallet.
397 ///
398 /// ## Dispatch Origin
399 ///
400 /// The dispatch origin of this call must be [`Config::ControlOrigin`].
401 ///
402 /// ## Details
403 ///
404 /// Can set the number of eras to check per block, and potentially other admin work.
405 ///
406 /// ## Events
407 ///
408 /// No events are emitted from this dispatch.
409 #[pallet::call_index(2)]
410 #[pallet::weight(<T as Config>::WeightInfo::control())]
411 pub fn control(origin: OriginFor<T>, eras_to_check: EraIndex) -> DispatchResult {
412 T::ControlOrigin::ensure_origin(origin)?;
413 ensure!(eras_to_check <= T::MaxErasToCheckPerBlock::get(), Error::<T>::CallNotAllowed);
414 ErasToCheckPerBlock::<T>::put(eras_to_check);
415 Ok(())
416 }
417 }
418
419 impl<T: Config> Pallet<T> {
420 /// Returns `true` if `staker` is anywhere to be found in the `head`.
421 pub(crate) fn is_head(staker: &T::AccountId) -> bool {
422 Head::<T>::get().map_or(false, |UnstakeRequest { stashes, .. }| {
423 stashes.iter().any(|(stash, _)| stash == staker)
424 })
425 }
426
427 /// Halt the operations of this pallet.
428 pub(crate) fn halt(reason: &'static str) {
429 frame_support::defensive!(reason);
430 ErasToCheckPerBlock::<T>::put(0);
431 Self::deposit_event(Event::<T>::InternalError)
432 }
433
434 /// process up to `remaining_weight`.
435 ///
436 /// Returns the actual weight consumed.
437 ///
438 /// Written for readability in mind, not efficiency. For example:
439 ///
440 /// 1. We assume this is only ever called once per `on_idle`. This is because we know that
441 /// in all use cases, even a single nominator cannot be unbonded in a single call. Multiple
442 /// calls to this function are thus not needed.
443 ///
444 /// 2. We will only mark a staker as unstaked if at the beginning of a check cycle, they are
445 /// found out to have no eras to check. At the end of a check cycle, even if they are fully
446 /// checked, we don't finish the process.
447 pub(crate) fn do_on_idle(remaining_weight: Weight) -> Weight {
448 // any weight that is unaccounted for
449 let mut unaccounted_weight = Weight::from_parts(0, 0);
450
451 let eras_to_check_per_block = ErasToCheckPerBlock::<T>::get();
452 if eras_to_check_per_block.is_zero() {
453 return T::DbWeight::get().reads(1).saturating_add(unaccounted_weight)
454 }
455
456 // NOTE: here we're assuming that the number of validators has only ever increased,
457 // meaning that the number of exposures to check is either this per era, or less.
458 let validator_count = T::Staking::desired_validator_count();
459 let (next_batch_size, reads_from_queue) = Head::<T>::get()
460 .map_or((Queue::<T>::count().min(T::BatchSize::get()), true), |head| {
461 (head.stashes.len() as u32, false)
462 });
463
464 // determine the number of eras to check. This is based on both `ErasToCheckPerBlock`
465 // and `remaining_weight` passed on to us from the runtime executive.
466 let max_weight = |v, b| {
467 // NOTE: this potentially under-counts by up to `BatchSize` reads from the queue.
468 <T as Config>::WeightInfo::on_idle_check(v, b)
469 .max(<T as Config>::WeightInfo::on_idle_unstake(b))
470 .saturating_add(if reads_from_queue {
471 T::DbWeight::get().reads(next_batch_size.into())
472 } else {
473 Zero::zero()
474 })
475 };
476
477 if max_weight(validator_count, next_batch_size).any_gt(remaining_weight) {
478 log!(debug, "early exit because eras_to_check_per_block is zero");
479 return T::DbWeight::get().reads(3).saturating_add(unaccounted_weight)
480 }
481
482 if T::Staking::election_ongoing() {
483 // NOTE: we assume `ongoing` does not consume any weight.
484 // there is an ongoing election -- we better not do anything. Imagine someone is not
485 // exposed anywhere in the last era, and the snapshot for the election is already
486 // taken. In this time period, we don't want to accidentally unstake them.
487 return T::DbWeight::get().reads(4).saturating_add(unaccounted_weight)
488 }
489
490 let UnstakeRequest { stashes, mut checked } = match Head::<T>::take().or_else(|| {
491 // NOTE: there is no order guarantees in `Queue`.
492 let stashes: BoundedVec<_, T::BatchSize> = Queue::<T>::drain()
493 .take(T::BatchSize::get() as usize)
494 .collect::<Vec<_>>()
495 .try_into()
496 .expect("take ensures bound is met; qed");
497 unaccounted_weight.saturating_accrue(
498 T::DbWeight::get().reads_writes(stashes.len() as u64, stashes.len() as u64),
499 );
500 if stashes.is_empty() {
501 None
502 } else {
503 Some(UnstakeRequest { stashes, checked: Default::default() })
504 }
505 }) {
506 None => {
507 // There's no `Head` and nothing in the `Queue`, nothing to do here.
508 return T::DbWeight::get().reads(4)
509 },
510 Some(head) => head,
511 };
512
513 log!(
514 debug,
515 "checking {:?} stashes, eras_to_check_per_block = {:?}, checked {:?}, remaining_weight = {:?}",
516 stashes.len(),
517 eras_to_check_per_block,
518 checked,
519 remaining_weight,
520 );
521
522 // the range that we're allowed to check in this round.
523 let current_era = T::Staking::current_era();
524 let bonding_duration = T::Staking::bonding_duration();
525
526 // prune all the old eras that we don't care about. This will help us keep the bound
527 // of `checked`.
528 checked.retain(|e| *e >= current_era.saturating_sub(bonding_duration));
529
530 let unchecked_eras_to_check = {
531 // get the last available `bonding_duration` eras up to current era in reverse
532 // order.
533 let total_check_range = (current_era.saturating_sub(bonding_duration)..=
534 current_era)
535 .rev()
536 .collect::<Vec<_>>();
537 debug_assert!(
538 total_check_range.len() <= (bonding_duration + 1) as usize,
539 "{:?}",
540 total_check_range
541 );
542
543 // remove eras that have already been checked, take a maximum of
544 // eras_to_check_per_block.
545 total_check_range
546 .into_iter()
547 .filter(|e| !checked.contains(e))
548 .take(eras_to_check_per_block as usize)
549 .collect::<Vec<_>>()
550 };
551
552 log!(
553 debug,
554 "{} eras to check: {:?}",
555 unchecked_eras_to_check.len(),
556 unchecked_eras_to_check
557 );
558
559 let unstake_stash = |stash: T::AccountId, deposit| {
560 let result = T::Staking::force_unstake(stash.clone());
561 let remaining = T::Currency::unreserve(&stash, deposit);
562 if !remaining.is_zero() {
563 Self::halt("not enough balance to unreserve");
564 } else {
565 log!(debug, "unstaked {:?}, outcome: {:?}", stash, result);
566 Self::deposit_event(Event::<T>::Unstaked { stash, result });
567 }
568 };
569
570 let check_stash = |stash, deposit| {
571 let is_exposed = unchecked_eras_to_check
572 .iter()
573 .any(|e| T::Staking::is_exposed_in_era(&stash, e));
574
575 if is_exposed {
576 let _ = T::Currency::slash_reserved(&stash, deposit);
577 log!(info, "slashed {:?} by {:?}", stash, deposit);
578 Self::deposit_event(Event::<T>::Slashed { stash, amount: deposit });
579 false
580 } else {
581 true
582 }
583 };
584
585 if unchecked_eras_to_check.is_empty() {
586 // `stashes` are not exposed in any era now -- we can let go of them now.
587 let size = stashes.len() as u32;
588 stashes.into_iter().for_each(|(stash, deposit)| unstake_stash(stash, deposit));
589 Self::deposit_event(Event::<T>::BatchFinished { size });
590 <T as Config>::WeightInfo::on_idle_unstake(size).saturating_add(unaccounted_weight)
591 } else {
592 let pre_length = stashes.len();
593 let stashes: BoundedVec<(T::AccountId, BalanceOf<T>), T::BatchSize> = stashes
594 .into_iter()
595 .filter(|(stash, deposit)| check_stash(stash.clone(), *deposit))
596 .collect::<Vec<_>>()
597 .try_into()
598 .expect("filter can only lessen the length; still in bound; qed");
599 let post_length = stashes.len();
600
601 log!(
602 debug,
603 "checked {:?}, pre stashes: {:?}, post: {:?}",
604 unchecked_eras_to_check,
605 pre_length,
606 post_length,
607 );
608
609 match checked.try_extend(unchecked_eras_to_check.clone().into_iter()) {
610 Ok(_) =>
611 if stashes.is_empty() {
612 Self::deposit_event(Event::<T>::BatchFinished { size: 0 });
613 } else {
614 Head::<T>::put(UnstakeRequest { stashes, checked });
615 Self::deposit_event(Event::<T>::BatchChecked {
616 eras: unchecked_eras_to_check,
617 });
618 },
619 Err(_) => {
620 // don't put the head back in -- there is an internal error in the pallet.
621 Self::halt("checked is pruned via retain above")
622 },
623 }
624
625 <T as Config>::WeightInfo::on_idle_check(validator_count, pre_length as u32)
626 .saturating_add(unaccounted_weight)
627 }
628 }
629 }
630}