Expand description

Made with Substrate, for Polkadot.

github - polkadot

Fast Unstake Pallet

A pallet to allow participants of the staking system (represented by Config::Staking, being StakingInterface) to unstake quicker, if and only if they meet the condition of not being exposed to any slashes.

Overview

If a nominator is not exposed anywhere in the staking system, checked via StakingInterface::is_exposed_in_era (i.e. “has not actively backed any validators in the last StakingInterface::bonding_duration days”), then they can register themselves in this pallet and unstake faster than having to wait an entire bonding duration.

Being exposed with validator from the point of view of the staking system means earning rewards with the validator, and also being at the risk of slashing with the validator. This is equivalent to the “Active Nominator” role explained in here.

Stakers who are certain about NOT being exposed can register themselves with Pallet::register_fast_unstake. This will chill, fully unbond the staker and place them in the queue to be checked.

A successful registration implies being fully unbonded and chilled in the staking system. These effects persist even if the fast-unstake registration is retracted (see Pallet::deregister and further).

Once registered as a fast-unstaker, the staker will be queued and checked by the system. This can take a variable number of blocks based on demand, but will almost certainly be “faster” (as the name suggest) than waiting the standard bonding duration.

A fast-unstaker is either in Queue or actively being checked, at which point it lives in Head. Once in Head, the request cannot be retracted anymore. But, once in Queue, it can, via Pallet::deregister.

A deposit equal to Config::Deposit is collected for this process, and is returned in case a successful unstake occurs (Event::Unstaked signals that).

Once processed, if successful, no additional fee for the checking process is taken, and the staker is instantly unbonded.

If unsuccessful, meaning that the staker was exposed, the aforementioned deposit will be slashed for the amount of wasted work they have inflicted on the chain.

All in all, this pallet is meant to provide an easy off-ramp for some stakers.

Example

  1. Fast-unstake with multiple participants in the queue.
	#[test]
	fn successful_multi_queue() {
		ExtBuilder::default().build_and_execute(|| {
			ErasToCheckPerBlock::<T>::put(BondingDuration::get() + 1);
			CurrentEra::<T>::put(BondingDuration::get());

			// register multi accounts for fast unstake
			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(1)));
			assert_eq!(Queue::<T>::get(1), Some(Deposit::get()));
			assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(3)));
			assert_eq!(Queue::<T>::get(3), Some(Deposit::get()));

			// assert 2 queue items are in Queue & None in Head to start with
			assert_eq!(Queue::<T>::count(), 2);
			assert_eq!(Head::<T>::get(), None);

			// process on idle and check eras for next Queue item
			next_block(true);

			// process on idle & let go of current Head
			next_block(true);

			// confirm Head / Queue items remaining
			assert_eq!(Queue::<T>::count(), 1);
			assert_eq!(Head::<T>::get(), None);

			// process on idle and check eras for next Queue item
			next_block(true);

			// process on idle & let go of current Head
			next_block(true);

			// Head & Queue should now be empty
			assert_eq!(Head::<T>::get(), None);
			assert_eq!(Queue::<T>::count(), 0);

			assert_eq!(
				fast_unstake_events_since_last_call(),
				vec![
					Event::BatchChecked { eras: vec![3, 2, 1, 0] },
					Event::Unstaked { stash: 1, result: Ok(()) },
					Event::BatchFinished { size: 1 },
					Event::BatchChecked { eras: vec![3, 2, 1, 0] },
					Event::Unstaked { stash: 3, result: Ok(()) },
					Event::BatchFinished { size: 1 },
				]
			);

			assert_unstaked(&1);
			assert_unstaked(&3);
		});
	}
  1. Fast unstake failing because a nominator is exposed.
	#[test]
	fn exposed_nominator_cannot_unstake() {
		ExtBuilder::default().build_and_execute(|| {
			ErasToCheckPerBlock::<T>::put(1);
			CurrentEra::<T>::put(BondingDuration::get());

			// create an exposed nominator in era 1
			let exposed = 666;
			create_exposed_nominator(exposed, 1);

			// a few blocks later, we realize they are slashed
			next_block(true);
			assert_eq!(
				Head::<T>::get(),
				Some(UnstakeRequest {
					stashes: bounded_vec![(exposed, Deposit::get())],
					checked: bounded_vec![3]
				})
			);
			next_block(true);
			assert_eq!(
				Head::<T>::get(),
				Some(UnstakeRequest {
					stashes: bounded_vec![(exposed, Deposit::get())],
					checked: bounded_vec![3, 2]
				})
			);
			next_block(true);
			assert_eq!(Head::<T>::get(), None);

			assert_eq!(
				fast_unstake_events_since_last_call(),
				vec![
					Event::BatchChecked { eras: vec![3] },
					Event::BatchChecked { eras: vec![2] },
					Event::Slashed { stash: exposed, amount: Deposit::get() },
					Event::BatchFinished { size: 0 }
				]
			);
		});
	}

Pallet API

See the pallet module for more information about the interfaces this pallet exposes, including its configuration trait, dispatchables, storage items, events and errors.

Low Level / Implementation Details

This pallet works off the basis of on_idle, meaning that it provides no guarantee about when it will succeed, if at all. Moreover, the queue implementation is unordered. In case of congestion, no FIFO ordering is provided.

A few important considerations can be concluded based on the on_idle-based implementation:

  • It is crucial for the weights of this pallet to be correct. The code inside Pallet::on_idle MUST be able to measure itself and report the remaining weight correctly after execution.

  • If the weight measurement is incorrect, it can lead to perpetual overweight (consequently slow) blocks.

  • The amount of weight that on_idle consumes is a direct function of ErasToCheckPerBlock.

  • Thus, a correct value of ErasToCheckPerBlock (which can be set via Pallet::control) should be chosen, such that a reasonable amount of weight is used on_idle. If ErasToCheckPerBlock is too large, on_idle will always conclude that it has not enough weight to proceed, and will early-return. Nonetheless, this should also be safe as long as the benchmarking/weights are accurate.

  • See the inline code-comments on do_on_idle (private) for more details.

  • For further safety, in case of any unforeseen errors, the pallet will emit Event::InternalError and set ErasToCheckPerBlock back to 0, which essentially means the pallet will halt/disable itself.

Re-exports

Modules

  • The pallet module in each FRAME pallet hosts the most important items needed to construct this pallet.
  • Types used in the Fast Unstake pallet.
  • Autogenerated weights for pallet_fast_unstake

Macros

Constants

Traits

  • The pallet hooks trait. This is merely an umbrella trait for:
  • A generic representation of a staking implementation.