15
15
// See the License for the specific language governing permissions and
16
16
// limitations under the License.
17
17
18
- //! A pallet that's designed to JUST do the following:
18
+ //! > Made with *Substrate*, for *Polkadot*.
19
19
//!
20
- //! If a nominator is not exposed in any `ErasStakers` (i.e. "has not actively backed any
21
- //! validators in the last `BondingDuration` days"), then they can register themselves in this
22
- //! pallet, unstake faster than having to wait an entire bonding duration.
20
+ //! [![github]](https://github.com/paritytech/substrate/frame/fast-unstake) -
21
+ //! [![polkadot]](https://polkadot.network)
23
22
//!
24
- //! Appearing in the exposure of a validator means being exposed equal to that validator from the
25
- //! point of view of the staking system. This usually means earning rewards with the validator, and
26
- //! also being at the risk of slashing with the validator. This is equivalent to the "Active
27
- //! Nominator" role explained in the
28
- //! [February Staking Update](https://polkadot.network/blog/staking-update-february-2022/).
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
29
25
//!
30
- //! This pallet works off the basis of `on_idle`, meaning that it provides no guarantee about when
31
- //! it will succeed, if at all. Moreover, the queue implementation is unordered. In case of
32
- //! congestion, no FIFO ordering is provided.
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.network/blog/staking-update-february-2022/).
33
43
//!
34
44
//! Stakers who are certain about NOT being exposed can register themselves with
35
- //! [`Call ::register_fast_unstake`]. This will chill, and fully unbond the staker, and place them in
36
- //! the queue to be checked.
45
+ //! [`Pallet ::register_fast_unstake`]. This will chill, fully unbond the staker and place them
46
+ //! in the queue to be checked.
37
47
//!
38
- //! Once queued, but not being actively processed, stakers can withdraw their request via
39
- //! [`Call::deregister`].
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).
40
51
//!
41
- //! Once queued, a staker wishing to unbond can perform no further action in pallet-staking. This is
42
- //! to prevent them from accidentally exposing themselves behind a validator etc.
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).
43
62
//!
44
63
//! Once processed, if successful, no additional fee for the checking process is taken, and the
45
64
//! staker is instantly unbonded.
46
65
//!
47
- //! If unsuccessful, meaning that the staker was exposed sometime in the last `BondingDuration` eras
48
- //! they will end up being slashed for the amount of wasted work they have inflicted on the chian.
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!( "frame/fast-unstake/src/tests.rs" , successful_multi_queue) ]
75
+ //!
76
+ //! 2. Fast unstake failing because a nominator is exposed.
77
+ #![ doc = docify:: embed!( "frame/fast-unstake/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.
49
112
50
113
#![ cfg_attr( not( feature = "std" ) , no_std) ]
51
114
@@ -64,9 +127,15 @@ pub mod migrations;
64
127
pub mod types;
65
128
pub mod weights;
66
129
130
+ // some extra imports for docs to link properly.
131
+ #[ cfg( doc) ]
132
+ pub use frame_support:: traits:: Hooks ;
133
+ #[ cfg( doc) ]
134
+ pub use sp_staking:: StakingInterface ;
135
+
136
+ /// The logging target of this pallet.
67
137
pub const LOG_TARGET : & ' static str = "runtime::fast-unstake" ;
68
138
69
- // syntactic sugar for logging.
70
139
#[ macro_export]
71
140
macro_rules! log {
72
141
( $level: tt, $patter: expr $( , $values: expr) * $( , ) ?) => {
@@ -94,16 +163,6 @@ pub mod pallet {
94
163
#[ cfg( feature = "try-runtime" ) ]
95
164
use sp_runtime:: TryRuntimeError ;
96
165
97
- #[ derive( scale_info:: TypeInfo , codec:: Encode , codec:: Decode , codec:: MaxEncodedLen ) ]
98
- #[ codec( mel_bound( T : Config ) ) ]
99
- #[ scale_info( skip_type_params( T ) ) ]
100
- pub struct MaxChecking < T : Config > ( sp_std:: marker:: PhantomData < T > ) ;
101
- impl < T : Config > frame_support:: traits:: Get < u32 > for MaxChecking < T > {
102
- fn get ( ) -> u32 {
103
- T :: Staking :: bonding_duration ( ) + 1
104
- }
105
- }
106
-
107
166
const STORAGE_VERSION : StorageVersion = StorageVersion :: new ( 1 ) ;
108
167
109
168
#[ pallet:: pallet]
@@ -125,7 +184,7 @@ pub mod pallet {
125
184
#[ pallet:: constant]
126
185
type Deposit : Get < BalanceOf < Self > > ;
127
186
128
- /// The origin that can control this pallet.
187
+ /// The origin that can control this pallet, in other words invoke [`Pallet::control`] .
129
188
type ControlOrigin : frame_support:: traits:: EnsureOrigin < Self :: RuntimeOrigin > ;
130
189
131
190
/// Batch size.
@@ -136,56 +195,61 @@ pub mod pallet {
136
195
/// The access to staking functionality.
137
196
type Staking : StakingInterface < Balance = BalanceOf < Self > , AccountId = Self :: AccountId > ;
138
197
198
+ /// Maximum value for `ErasToCheckPerBlock`, checked in [`Pallet::control`].
199
+ ///
200
+ /// This should be slightly bigger than the actual value in order to have accurate
201
+ /// benchmarks.
202
+ type MaxErasToCheckPerBlock : Get < u32 > ;
203
+
139
204
/// The weight information of this pallet.
140
205
type WeightInfo : WeightInfo ;
141
206
142
- /// Maximum value for `ErasToCheckPerBlock`. This should be as close as possible, but more
143
- /// than the actual value, in order to have accurate benchmarks.
144
- type MaxErasToCheckPerBlock : Get < u32 > ;
145
-
146
207
/// Use only for benchmarking.
147
208
#[ cfg( feature = "runtime-benchmarks" ) ]
148
209
type MaxBackersPerValidator : Get < u32 > ;
149
210
}
150
211
151
212
/// The current "head of the queue" being unstaked.
213
+ ///
214
+ /// The head in itself can be a batch of up to [`Config::BatchSize`] stakers.
152
215
#[ pallet:: storage]
153
216
pub type Head < T : Config > = StorageValue < _ , UnstakeRequest < T > , OptionQuery > ;
154
217
155
218
/// The map of all accounts wishing to be unstaked.
156
219
///
157
220
/// Keeps track of `AccountId` wishing to unstake and it's corresponding deposit.
158
- ///
159
- /// TWOX-NOTE: SAFE since `AccountId` is a secure hash.
221
+ // Hasher: Twox safe since `AccountId` is a secure hash.
160
222
#[ pallet:: storage]
161
223
pub type Queue < T : Config > = CountedStorageMap < _ , Twox64Concat , T :: AccountId , BalanceOf < T > > ;
162
224
163
225
/// Number of eras to check per block.
164
226
///
165
- /// If set to 0, this pallet does absolutely nothing.
227
+ /// If set to 0, this pallet does absolutely nothing. Cannot be set to more than
228
+ /// [`Config::MaxErasToCheckPerBlock`].
166
229
///
167
- /// Based on the amount of weight available at `on_idle`, up to this many eras of a single
168
- /// nominator might be checked.
230
+ /// Based on the amount of weight available at [`Pallet::on_idle`], up to this many eras are
231
+ /// checked. The checking is represented by updating [`UnstakeRequest::checked`], which is
232
+ /// stored in [`Head`].
169
233
#[ pallet:: storage]
234
+ #[ pallet:: getter( fn eras_to_check_per_block) ]
170
235
pub type ErasToCheckPerBlock < T : Config > = StorageValue < _ , u32 , ValueQuery > ;
171
236
172
- /// The events of this pallet.
173
237
#[ pallet:: event]
174
238
#[ pallet:: generate_deposit( pub ( super ) fn deposit_event) ]
175
239
pub enum Event < T : Config > {
176
240
/// A staker was unstaked.
177
241
Unstaked { stash : T :: AccountId , result : DispatchResult } ,
178
242
/// A staker was slashed for requesting fast-unstake whilst being exposed.
179
243
Slashed { stash : T :: AccountId , amount : BalanceOf < T > } ,
180
- /// An internal error happened. Operations will be paused now.
181
- InternalError ,
182
244
/// A batch was partially checked for the given eras, but the process did not finish.
183
245
BatchChecked { eras : Vec < EraIndex > } ,
184
246
/// A batch of a given size was terminated.
185
247
///
186
248
/// This is always follows by a number of `Unstaked` or `Slashed` events, marking the end
187
249
/// of the batch. A new batch will be created upon next block.
188
250
BatchFinished { size : u32 } ,
251
+ /// An internal error happened. Operations will be paused now.
252
+ InternalError ,
189
253
}
190
254
191
255
#[ pallet:: error]
@@ -247,8 +311,12 @@ pub mod pallet {
247
311
impl < T : Config > Pallet < T > {
248
312
/// Register oneself for fast-unstake.
249
313
///
250
- /// The dispatch origin of this call must be signed by the controller account, similar to
251
- /// `staking::unbond`.
314
+ /// ## Dispatch Origin
315
+ ///
316
+ /// The dispatch origin of this call must be *signed* by whoever is permitted to call
317
+ /// unbond funds by the staking system. See [`Config::Staking`].
318
+ ///
319
+ /// ## Details
252
320
///
253
321
/// The stash associated with the origin must have no ongoing unlocking chunks. If
254
322
/// successful, this will fully unbond and chill the stash. Then, it will enqueue the stash
@@ -263,6 +331,10 @@ pub mod pallet {
263
331
/// If the check fails, the stash remains chilled and waiting for being unbonded as in with
264
332
/// the normal staking system, but they lose part of their unbonding chunks due to consuming
265
333
/// the chain's resources.
334
+ ///
335
+ /// ## Events
336
+ ///
337
+ /// Some events from the staking and currency system might be emitted.
266
338
#[ pallet:: call_index( 0 ) ]
267
339
#[ pallet:: weight( <T as Config >:: WeightInfo :: register_fast_unstake( ) ) ]
268
340
pub fn register_fast_unstake ( origin : OriginFor < T > ) -> DispatchResult {
@@ -288,11 +360,22 @@ pub mod pallet {
288
360
289
361
/// Deregister oneself from the fast-unstake.
290
362
///
363
+ /// ## Dispatch Origin
364
+ ///
365
+ /// The dispatch origin of this call must be *signed* by whoever is permitted to call
366
+ /// unbond funds by the staking system. See [`Config::Staking`].
367
+ ///
368
+ /// ## Details
369
+ ///
291
370
/// This is useful if one is registered, they are still waiting, and they change their mind.
292
371
///
293
372
/// Note that the associated stash is still fully unbonded and chilled as a consequence of
294
- /// calling `register_fast_unstake`. This should probably be followed by a call to
295
- /// `Staking::rebond`.
373
+ /// calling [`Pallet::register_fast_unstake`]. Therefore, this should probably be followed
374
+ /// by a call to `rebond` in the staking system.
375
+ ///
376
+ /// ## Events
377
+ ///
378
+ /// Some events from the staking and currency system might be emitted.
296
379
#[ pallet:: call_index( 1 ) ]
297
380
#[ pallet:: weight( <T as Config >:: WeightInfo :: deregister( ) ) ]
298
381
pub fn deregister ( origin : OriginFor < T > ) -> DispatchResult {
@@ -318,7 +401,17 @@ pub mod pallet {
318
401
319
402
/// Control the operation of this pallet.
320
403
///
321
- /// Dispatch origin must be signed by the [`Config::ControlOrigin`].
404
+ /// ## Dispatch Origin
405
+ ///
406
+ /// The dispatch origin of this call must be [`Config::ControlOrigin`].
407
+ ///
408
+ /// ## Details
409
+ ///
410
+ /// Can set the number of eras to check per block, and potentially other admin work.
411
+ ///
412
+ /// ## Events
413
+ ///
414
+ /// No events are emitted from this dispatch.
322
415
#[ pallet:: call_index( 2 ) ]
323
416
#[ pallet:: weight( <T as Config >:: WeightInfo :: control( ) ) ]
324
417
pub fn control ( origin : OriginFor < T > , eras_to_check : EraIndex ) -> DispatchResult {
0 commit comments