referrerpolicy=no-referrer-when-downgrade

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}