referrerpolicy=no-referrer-when-downgrade

pallet_staking_async/
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//! # Staking Async Pallet
19//!
20//! This pallet is a fork of the original `pallet-staking`, with a number of key differences:
21//!
22//! * It no longer has access to a secure timestamp, previously used to calculate the duration of an
23//!   era.
24//! * It no longer has access to a pallet-session.
25//! * It no longer has access to a pallet-authorship.
26//! * It is capable of working with a multi-page `ElectionProvider``, aka.
27//!   `pallet-election-provider-multi-block`.
28//!
29//! While `pallet-staking` was somewhat general-purpose, this pallet is absolutely NOT right from
30//! the get-go: It is designed to be used ONLY in Polkadot/Kusama AssetHub system parachains.
31//!
32//! The workings of this pallet can be divided into a number of subsystems, as follows.
33//!
34//! ## User Interactions
35//!
36//! TODO
37//!
38//! ## Session and Era Rotation
39//!
40//! TODO
41//!
42//! ## Exposure Collection
43//!
44//! TODO
45//!
46//! ## Slashing Pipeline and Withdrawal Restrictions
47//!
48//! This pallet implements a robust slashing mechanism that ensures the integrity of the staking
49//! system while preventing stakers from withdrawing funds that might still be subject to slashing.
50//!
51//! ### Overview of the Slashing Pipeline
52//!
53//! The slashing process consists of multiple phases:
54//!
55//! 1. **Offence Reporting**: Offences are reported from the relay chain through `on_new_offences`
56//! 2. **Queuing**: Valid offences are added to the `OffenceQueue` for processing
57//! 3. **Processing**: Offences are processed incrementally over multiple blocks
58//! 4. **Application**: Slashes are either applied immediately or deferred based on configuration
59//!
60//! ### Phase 1: Offence Reporting
61//!
62//! Offences are reported from the relay chain (e.g., from BABE, GRANDPA, BEEFY, or parachain
63//! modules) through the `on_new_offences` function:
64//!
65//! ```text
66//! struct Offence {
67//!     offender: AccountId,        // The validator being slashed
68//!     reporters: Vec<AccountId>,  // Who reported the offence (may be empty)
69//!     slash_fraction: Perbill,    // Percentage of stake to slash
70//! }
71//! ```
72//!
73//! **Reporting Deadlines**:
74//! - With deferred slashing: Offences must be reported within `SlashDeferDuration - 1` eras
75//! - With immediate slashing: Offences can be reported up to `BondingDuration` eras old
76//!
77//! Example: If `SlashDeferDuration = 27` and current era is 100:
78//! - Oldest reportable offence: Era 74 (100 - 26)
79//! - Offences from era 73 or earlier are rejected
80//!
81//! ### Phase 2: Queuing
82//!
83//! When an offence passes validation, it's added to the queue:
84//!
85//! 1. **Storage**: Added to `OffenceQueue`: `(EraIndex, AccountId) -> OffenceRecord`
86//! 2. **Era Tracking**: Era added to `OffenceQueueEras` (sorted vector of eras with offences)
87//! 3. **Duplicate Handling**: If an offence already exists for the same validator in the same era,
88//!    only the higher slash fraction is kept
89//!
90//! ### Phase 3: Processing
91//!
92//! Offences are processed incrementally in `on_initialize` each block:
93//!
94//! ```text
95//! 1. Load oldest offence from queue
96//! 2. Move to `ProcessingOffence` storage
97//! 3. For each exposure page (from last to first):
98//!    - Calculate slash for validator's own stake
99//!    - Calculate slash for each nominator (pro-rata based on exposure)
100//!    - Track total slash and reward amounts
101//! 4. Once all pages processed, create `UnappliedSlash`
102//! ```
103//!
104//! **Key Features**:
105//! - **Page-by-page processing**: Large validator sets don't overwhelm a single block
106//! - **Pro-rata slashing**: Nominators slashed proportionally to their stake
107//! - **Reward calculation**: A portion goes to reporters (if any)
108//!
109//! ### Phase 4: Application
110//!
111//! Based on `SlashDeferDuration`, slashes are either:
112//!
113//! **Immediate (SlashDeferDuration = 0)**:
114//! - Applied right away in the same block
115//! - Funds deducted from staking ledger immediately
116//!
117//! **Deferred (SlashDeferDuration > 0)**:
118//! - Stored in `UnappliedSlashes` for future application
119//! - Applied at era: `offence_era + SlashDeferDuration`
120//! - Can be cancelled by governance before application
121//!
122//! ### Storage Items Involved
123//!
124//! - `OffenceQueue`: Pending offences to process
125//! - `OffenceQueueEras`: Sorted list of eras with offences
126//! - `ProcessingOffence`: Currently processing offence
127//! - `ValidatorSlashInEra`: Tracks highest slash per validator per era
128//! - `UnappliedSlashes`: Deferred slashes waiting for application
129//!
130//! ### Withdrawal Restrictions
131//!
132//! To maintain slashing guarantees, withdrawals are restricted:
133//!
134//! **Withdrawal Era Calculation**:
135//! ```text
136//! earliest_era_to_withdraw = min(
137//!     active_era,
138//!     last_fully_processed_offence_era + BondingDuration
139//! )
140//! ```
141//!
142//! **Example**:
143//! - Active era: 100
144//! - Oldest unprocessed offence: Era 70
145//! - BondingDuration: 28
146//! - Withdrawal allowed only for chunks with era ≤ 97 (70 - 1 + 28)
147//!
148//! **Withdrawal Timeline Example with an Offence**:
149//! ```text
150//! Era:        90    91    92    93    94    95    96    97    98    99    100   ...  117   118
151//!             |     |     |     |     |     |     |     |     |     |     |          |     |
152//! Unbond:     U
153//! Offence:    X
154//! Reported:               R
155//! Processed:              P (within next few blocks)
156//! Slash Applied:                                                                       S
157//! Withdraw:                                                                            ❌    ✓
158//!
159//! With BondingDuration = 28 and SlashDeferDuration = 27:
160//! - User unbonds in era 90
161//! - Offence occurs in era 90
162//! - Reported in era 92 (typically within 2 days, but reportable until Era 116)
163//! - Processed in era 92 (within next few blocks after reporting)
164//! - Slash deferred for 27 eras, applied at era 117 (90 + 27)
165//! - Cannot withdraw unbonded chunks until era 118 (90 + 28)
166//!
167//! The 28-era bonding duration ensures that any offences committed before or during
168//! unbonding have time to be reported, processed, and applied before funds can be
169//! withdrawn. This provides a window for governance to cancel slashes that may have
170//! resulted from software bugs.
171//! ```
172//!
173//! **Key Restrictions**:
174//! 1. Cannot withdraw if previous era has unapplied slashes
175//! 2. Cannot withdraw funds from eras with unprocessed offences
176
177#![cfg_attr(not(feature = "std"), no_std)]
178#![recursion_limit = "256"]
179
180#[cfg(feature = "runtime-benchmarks")]
181pub mod benchmarking;
182#[cfg(any(feature = "runtime-benchmarks", test))]
183pub mod testing_utils;
184
185#[cfg(test)]
186pub(crate) mod mock;
187#[cfg(test)]
188mod tests;
189
190pub mod asset;
191pub mod election_size_tracker;
192pub mod ledger;
193mod pallet;
194pub mod session_rotation;
195pub mod slashing;
196pub mod weights;
197
198extern crate alloc;
199use alloc::{vec, vec::Vec};
200use codec::{Decode, DecodeWithMemTracking, Encode, HasCompact, MaxEncodedLen};
201use frame_election_provider_support::ElectionProvider;
202use frame_support::{
203	traits::{
204		tokens::fungible::{Credit, Debt},
205		ConstU32, Contains, Get, LockIdentifier,
206	},
207	BoundedVec, DebugNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
208	WeakBoundedVec,
209};
210use frame_system::pallet_prelude::BlockNumberFor;
211use ledger::LedgerIntegrityState;
212use scale_info::TypeInfo;
213use sp_runtime::{
214	traits::{AtLeast32BitUnsigned, One, StaticLookup, UniqueSaturatedInto},
215	BoundedBTreeMap, Perbill, RuntimeDebug, Saturating,
216};
217use sp_staking::{EraIndex, ExposurePage, PagedExposureMetadata, SessionIndex};
218pub use sp_staking::{Exposure, IndividualExposure, StakerStatus};
219pub use weights::WeightInfo;
220
221// public exports
222pub use ledger::{StakingLedger, UnlockChunk};
223pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap};
224
225pub(crate) const STAKING_ID: LockIdentifier = *b"staking ";
226pub(crate) const LOG_TARGET: &str = "runtime::staking-async";
227
228// syntactic sugar for logging.
229#[macro_export]
230macro_rules! log {
231	($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
232		log::$level!(
233			target: crate::LOG_TARGET,
234			concat!("[{:?}] 💸 ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
235		)
236	};
237}
238
239/// Alias for a bounded set of exposures behind a validator, parameterized by this pallet's
240/// election provider.
241pub type BoundedExposuresOf<T> = BoundedVec<
242	(
243		<T as frame_system::Config>::AccountId,
244		Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
245	),
246	MaxWinnersPerPageOf<<T as Config>::ElectionProvider>,
247>;
248
249/// Alias for the maximum number of winners (aka. active validators), as defined in by this pallet's
250/// config.
251pub type MaxWinnersOf<T> = <T as Config>::MaxValidatorSet;
252
253/// Alias for the maximum number of winners per page, as expected by the election provider.
254pub type MaxWinnersPerPageOf<P> = <P as ElectionProvider>::MaxWinnersPerPage;
255
256/// Maximum number of nominations per nominator.
257pub type MaxNominationsOf<T> =
258	<<T as Config>::NominationsQuota as NominationsQuota<BalanceOf<T>>>::MaxNominations;
259
260/// Counter for the number of "reward" points earned by a given validator.
261pub type RewardPoint = u32;
262
263/// The balance type of this pallet.
264pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
265
266type PositiveImbalanceOf<T> = Debt<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
267pub type NegativeImbalanceOf<T> =
268	Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
269
270type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
271
272/// Information regarding the active era (era in used in session).
273#[derive(Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, PartialEq, Eq, Clone)]
274pub struct ActiveEraInfo {
275	/// Index of era.
276	pub index: EraIndex,
277	/// Moment of start expressed as millisecond from `$UNIX_EPOCH`.
278	///
279	/// Start can be none if start hasn't been set for the era yet,
280	/// Start is set on the first on_finalize of the era to guarantee usage of `Time`.
281	pub start: Option<u64>,
282}
283
284/// Reward points of an era. Used to split era total payout between validators.
285///
286/// This points will be used to reward validators and their respective nominators.
287#[derive(
288	PartialEqNoBound, Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen, DefaultNoBound,
289)]
290#[codec(mel_bound())]
291#[scale_info(skip_type_params(T))]
292pub struct EraRewardPoints<T: Config> {
293	/// Total number of points. Equals the sum of reward points for each validator.
294	pub total: RewardPoint,
295	/// The reward points earned by a given validator.
296	pub individual: BoundedBTreeMap<T::AccountId, RewardPoint, T::MaxValidatorSet>,
297}
298
299/// A destination account for payment.
300#[derive(
301	PartialEq,
302	Eq,
303	Copy,
304	Clone,
305	Encode,
306	Decode,
307	DecodeWithMemTracking,
308	RuntimeDebug,
309	TypeInfo,
310	MaxEncodedLen,
311)]
312pub enum RewardDestination<AccountId> {
313	/// Pay into the stash account, increasing the amount at stake accordingly.
314	Staked,
315	/// Pay into the stash account, not increasing the amount at stake.
316	Stash,
317	#[deprecated(
318		note = "`Controller` will be removed after January 2024. Use `Account(controller)` instead."
319	)]
320	Controller,
321	/// Pay into a specified account.
322	Account(AccountId),
323	/// Receive no reward.
324	None,
325}
326
327/// Preference of what happens regarding validation.
328#[derive(
329	PartialEq,
330	Eq,
331	Clone,
332	Encode,
333	Decode,
334	DecodeWithMemTracking,
335	RuntimeDebug,
336	TypeInfo,
337	Default,
338	MaxEncodedLen,
339)]
340pub struct ValidatorPrefs {
341	/// Reward that validator takes up-front; only the rest is split between themselves and
342	/// nominators.
343	#[codec(compact)]
344	pub commission: Perbill,
345	/// Whether or not this validator is accepting more nominations. If `true`, then no nominator
346	/// who is not already nominating this validator may nominate them. By default, validators
347	/// are accepting nominations.
348	pub blocked: bool,
349}
350
351/// Status of a paged snapshot progress.
352#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Default)]
353pub enum SnapshotStatus<AccountId> {
354	/// Paged snapshot is in progress, the `AccountId` was the last staker iterated in the list.
355	Ongoing(AccountId),
356	/// All the stakers in the system have been consumed since the snapshot started.
357	Consumed,
358	/// Waiting for a new snapshot to be requested.
359	#[default]
360	Waiting,
361}
362
363/// A record of the nominations made by a specific account.
364#[derive(
365	PartialEqNoBound, EqNoBound, Clone, Encode, Decode, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,
366)]
367#[codec(mel_bound())]
368#[scale_info(skip_type_params(T))]
369pub struct Nominations<T: Config> {
370	/// The targets of nomination.
371	pub targets: BoundedVec<T::AccountId, MaxNominationsOf<T>>,
372	/// The era the nominations were submitted.
373	///
374	/// Except for initial nominations which are considered submitted at era 0.
375	pub submitted_in: EraIndex,
376	/// Whether the nominations have been suppressed. This can happen due to slashing of the
377	/// validators, or other events that might invalidate the nomination.
378	///
379	/// NOTE: this for future proofing and is thus far not used.
380	pub suppressed: bool,
381}
382
383/// Facade struct to encapsulate `PagedExposureMetadata` and a single page of `ExposurePage`.
384///
385/// This is useful where we need to take into account the validator's own stake and total exposure
386/// in consideration, in addition to the individual nominators backing them.
387#[derive(Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)]
388pub struct PagedExposure<AccountId, Balance: HasCompact + codec::MaxEncodedLen> {
389	exposure_metadata: PagedExposureMetadata<Balance>,
390	exposure_page: ExposurePage<AccountId, Balance>,
391}
392
393impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEncodedLen>
394	PagedExposure<AccountId, Balance>
395{
396	/// Create a new instance of `PagedExposure` from legacy clipped exposures.
397	pub fn from_clipped(exposure: Exposure<AccountId, Balance>) -> Self {
398		Self {
399			exposure_metadata: PagedExposureMetadata {
400				total: exposure.total,
401				own: exposure.own,
402				nominator_count: exposure.others.len() as u32,
403				page_count: 1,
404			},
405			exposure_page: ExposurePage { page_total: exposure.total, others: exposure.others },
406		}
407	}
408
409	/// Returns total exposure of this validator across pages
410	pub fn total(&self) -> Balance {
411		self.exposure_metadata.total
412	}
413
414	/// Returns total exposure of this validator for the current page
415	pub fn page_total(&self) -> Balance {
416		self.exposure_page.page_total + self.exposure_metadata.own
417	}
418
419	/// Returns validator's own stake that is exposed
420	pub fn own(&self) -> Balance {
421		self.exposure_metadata.own
422	}
423
424	/// Returns the portions of nominators stashes that are exposed in this page.
425	pub fn others(&self) -> &Vec<IndividualExposure<AccountId, Balance>> {
426		&self.exposure_page.others
427	}
428}
429
430/// A pending slash record. The value of the slash has been computed but not applied yet,
431/// rather deferred for several eras.
432#[derive(Encode, Decode, DebugNoBound, TypeInfo, MaxEncodedLen, PartialEqNoBound, EqNoBound)]
433#[scale_info(skip_type_params(T))]
434pub struct UnappliedSlash<T: Config> {
435	/// The stash ID of the offending validator.
436	pub validator: T::AccountId,
437	/// The validator's own slash.
438	pub own: BalanceOf<T>,
439	/// All other slashed stakers and amounts.
440	pub others: WeakBoundedVec<(T::AccountId, BalanceOf<T>), T::MaxExposurePageSize>,
441	/// Reporters of the offence; bounty payout recipients.
442	pub reporter: Option<T::AccountId>,
443	/// The amount of payout.
444	pub payout: BalanceOf<T>,
445}
446
447/// Something that defines the maximum number of nominations per nominator based on a curve.
448///
449/// The method `curve` implements the nomination quota curve and should not be used directly.
450/// However, `get_quota` returns the bounded maximum number of nominations based on `fn curve` and
451/// the nominator's balance.
452pub trait NominationsQuota<Balance> {
453	/// Strict maximum number of nominations that caps the nominations curve. This value can be
454	/// used as the upper bound of the number of votes per nominator.
455	type MaxNominations: Get<u32>;
456
457	/// Returns the voter's nomination quota within reasonable bounds [`min`, `max`], where `min`
458	/// is 1 and `max` is `Self::MaxNominations`.
459	fn get_quota(balance: Balance) -> u32 {
460		Self::curve(balance).clamp(1, Self::MaxNominations::get())
461	}
462
463	/// Returns the voter's nomination quota based on its balance and a curve.
464	fn curve(balance: Balance) -> u32;
465}
466
467/// A nomination quota that allows up to MAX nominations for all validators.
468pub struct FixedNominationsQuota<const MAX: u32>;
469impl<Balance, const MAX: u32> NominationsQuota<Balance> for FixedNominationsQuota<MAX> {
470	type MaxNominations = ConstU32<MAX>;
471
472	fn curve(_: Balance) -> u32 {
473		MAX
474	}
475}
476
477/// Handler for determining how much of a balance should be paid out on the current era.
478pub trait EraPayout<Balance> {
479	/// Determine the payout for this era.
480	///
481	/// Returns the amount to be paid to stakers in this era, as well as whatever else should be
482	/// paid out ("the rest").
483	fn era_payout(
484		total_staked: Balance,
485		total_issuance: Balance,
486		era_duration_millis: u64,
487	) -> (Balance, Balance);
488}
489
490impl<Balance: Default> EraPayout<Balance> for () {
491	fn era_payout(
492		_total_staked: Balance,
493		_total_issuance: Balance,
494		_era_duration_millis: u64,
495	) -> (Balance, Balance) {
496		(Default::default(), Default::default())
497	}
498}
499
500/// Mode of era-forcing.
501#[derive(
502	Copy,
503	Clone,
504	PartialEq,
505	Eq,
506	Encode,
507	Decode,
508	DecodeWithMemTracking,
509	RuntimeDebug,
510	TypeInfo,
511	MaxEncodedLen,
512	serde::Serialize,
513	serde::Deserialize,
514)]
515pub enum Forcing {
516	/// Not forcing anything - just let whatever happen.
517	NotForcing,
518	/// Force a new era, then reset to `NotForcing` as soon as it is done.
519	/// Note that this will force to trigger an election until a new era is triggered, if the
520	/// election failed, the next session end will trigger a new election again, until success.
521	ForceNew,
522	/// Avoid a new era indefinitely.
523	ForceNone,
524	/// Force a new era at the end of all sessions indefinitely.
525	ForceAlways,
526}
527
528impl Default for Forcing {
529	fn default() -> Self {
530		Forcing::NotForcing
531	}
532}
533
534/// A utility struct that provides a way to check if a given account is a staker.
535///
536/// This struct implements the `Contains` trait, allowing it to determine whether
537/// a particular account is currently staking by checking if the account exists in
538/// the staking ledger.
539///
540/// Intended to be used in [`crate::Config::Filter`].
541pub struct AllStakers<T: Config>(core::marker::PhantomData<T>);
542
543impl<T: Config> Contains<T::AccountId> for AllStakers<T> {
544	/// Checks if the given account ID corresponds to a staker.
545	///
546	/// # Returns
547	/// - `true` if the account has an entry in the staking ledger (indicating it is staking).
548	/// - `false` otherwise.
549	fn contains(account: &T::AccountId) -> bool {
550		Ledger::<T>::contains_key(account)
551	}
552}
553
554/// A smart type to determine the [`Config::PlanningEraOffset`], given:
555///
556/// * Expected relay session duration, `RS`
557/// * Time taking into consideration for XCM sending, `S`
558///
559/// It will use the estimated election duration, the relay session duration, and add one as it knows
560/// the relay chain will want to buffer validators for one session. This is needed because we use
561/// this in our calculation based on the "active era".
562pub struct PlanningEraOffsetOf<T, RS, S>(core::marker::PhantomData<(T, RS, S)>);
563impl<T: Config, RS: Get<BlockNumberFor<T>>, S: Get<BlockNumberFor<T>>> Get<SessionIndex>
564	for PlanningEraOffsetOf<T, RS, S>
565{
566	fn get() -> SessionIndex {
567		let election_duration = <T::ElectionProvider as ElectionProvider>::duration_with_export();
568		let sessions_needed = (election_duration + S::get()) / RS::get();
569		// add one, because we know the RC session pallet wants to buffer for one session, and
570		// another one cause we will receive activation report one session after that.
571		sessions_needed
572			.saturating_add(One::one())
573			.saturating_add(One::one())
574			.unique_saturated_into()
575	}
576}