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. 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}