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