Crate pallet_fast_unstake
source ·Expand description
Made with Substrate, for 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
- 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);
});
}
- 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 ofErasToCheckPerBlock
. -
Thus, a correct value of
ErasToCheckPerBlock
(which can be set viaPallet::control
) should be chosen, such that a reasonable amount of weight is usedon_idle
. IfErasToCheckPerBlock
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 setErasToCheckPerBlock
back to 0, which essentially means the pallet will halt/disable itself.
Re-exports
pub use pallet::*;
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
- The logging target of this pallet.
Traits
- The pallet hooks trait. This is merely an umbrella trait for:
- A generic representation of a staking implementation.