pallet_collator_selection/
lib.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.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//! Collator Selection pallet.
19//!
20//! A pallet to manage collators in a parachain with council-approved candidacy.
21//!
22//! ## Overview
23//!
24//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a
25//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT
26//! safety assumptions of the chosen set.
27//!
28//! ## Permissioned Candidacy System
29//!
30//! As of this version, collator candidacy requires governance approval:
31//! 1. Accounts must first apply via `apply_for_candidacy` (reserves bond)
32//! 2. Governance reviews and either approves via `approve_application` (directly adds to candidates)
33//!    or rejects via `close_application` (unreserves bond)
34//! 3. Governance can force-remove candidates with slashing via `kick_candidate`
35//! 4. Users can withdraw pending applications via `close_application` (only before processing)
36//!
37//! The old `register_as_candidate` extrinsic is deprecated and will fail with a Permission error.
38//!
39//! ## Terminology
40//!
41//! - Collator: A parachain block producer.
42//! - Bond: An amount of `Balance` _reserved_ for candidate registration.
43//! - Invulnerable: An account guaranteed to be in the collator set.
44//! - Pending Application: An application that has been submitted but not yet processed.
45//!
46//! ## Governance Origins
47//!
48//! The pallet uses two configurable origins for governance actions:
49//! - `GovernanceOrigin`: Can approve/close candidacy applications
50//! - `ForceRemovalOrigin`: Can forcibly remove active candidates
51//!
52//! ## Implementation
53//!
54//! The final `Collators` are aggregated from two individual lists:
55//!
56//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be
57//!    collators.
58//! 2. [`Candidates`]: these are *governance-approved candidates to the collation task* and may or may not be elected as
59//!    a final collator.
60//!
61//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve
62//! manner.
63//!
64//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates
65//! fall below MinCandidates. This is for potential disaster recovery scenarios.
66//!
67//! ### Rewards
68//!
69//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the
70//! collator who authored it receives:
71//!
72//! - Half the value of the Pot.
73//! - Half the value of the transaction fees within the block. The other half of the transaction
74//!   fees are deposited into the Pot.
75//!
76//! To initiate rewards an ED needs to be transferred to the pot address.
77//!
78//! Note: Eventually the Pot distribution may be modified as discussed in
79//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).
80
81#![cfg_attr(not(feature = "std"), no_std)]
82#![allow(clippy::useless_conversion)]
83
84pub use pallet::*;
85pub mod migrations;
86
87#[cfg(test)]
88mod mock;
89
90#[cfg(test)]
91mod tests;
92
93#[cfg(feature = "runtime-benchmarks")]
94mod benchmarking;
95pub mod weights;
96
97#[frame_support::pallet]
98pub mod pallet {
99    pub use crate::weights::WeightInfo;
100    use core::ops::Div;
101    use frame_support::{
102        dispatch::{DispatchClass, DispatchResultWithPostInfo},
103        ensure,
104        pallet_prelude::*,
105        sp_runtime::{
106            traits::{AccountIdConversion, CheckedSub, Saturating, Zero},
107            RuntimeDebug,
108        },
109        traits::{
110            Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,
111            ValidatorRegistration, ValidatorSet,
112        },
113        DefaultNoBound, PalletId,
114    };
115    use frame_system::{pallet_prelude::*, Config as SystemConfig};
116    use pallet_session::SessionManager;
117    use sp_runtime::{
118        traits::{BadOrigin, Convert},
119        Perbill,
120    };
121    use sp_staking::SessionIndex;
122    use sp_std::prelude::*;
123
124    type BalanceOf<T> =
125        <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
126
127    /// A convertor from collators id. Since this pallet does not have stash/controller, this is
128    /// just identity.
129    pub struct IdentityCollator;
130    impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {
131        fn convert(t: T) -> Option<T> {
132            Some(t)
133        }
134    }
135
136    /// Used to check whether an account is allowed to be a candidate.
137    pub trait AccountCheck<AccountId> {
138        /// `true` if the account is allowed to be a candidate, `false` otherwise.
139        fn allowed_candidacy(account: &AccountId) -> bool;
140    }
141
142    /// Configure the pallet by specifying the parameters and types on which it depends.
143    #[pallet::config]
144    pub trait Config: frame_system::Config {
145        /// The currency mechanism.
146        type Currency: ReservableCurrency<Self::AccountId>;
147
148        /// Origin that can dictate updating parameters of this pallet.
149        type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
150
151        /// Origin that can approve or reject candidacy applications.
152        /// Typically the governance council or root.
153        type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
154
155        /// Origin that can forcibly remove candidates from the active set.
156        /// Typically the same as GovernanceOrigin but separated for flexibility.
157        type ForceRemovalOrigin: EnsureOrigin<Self::RuntimeOrigin>;
158
159        /// Account Identifier from which the internal Pot is generated.
160        type PotId: Get<PalletId>;
161
162        /// Maximum number of candidates that we should have. This is used for benchmarking and is not
163        /// enforced.
164        ///
165        /// This does not take into account the invulnerables.
166        type MaxCandidates: Get<u32>;
167
168        /// Minimum number of candidates that we should have. This is used for disaster recovery.
169        ///
170        /// This does not take into account the invulnerables.
171        type MinCandidates: Get<u32>;
172
173        /// Maximum number of invulnerables.
174        ///
175        /// Used only for benchmarking.
176        type MaxInvulnerables: Get<u32>;
177
178        /// Will be kicked if block is not produced in threshold.
179        type KickThreshold: Get<BlockNumberFor<Self>>;
180
181        /// A stable ID for a validator.
182        type ValidatorId: Member + Parameter;
183
184        /// A conversion from account ID to validator ID.
185        ///
186        /// Its cost must be at most one storage read.
187        type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;
188
189        /// Validate a user is registered
190        type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;
191
192        /// Something that can give information about the current validator set.
193        type ValidatorSet: ValidatorSet<Self::AccountId, ValidatorId = Self::AccountId>;
194
195        /// How many in perc kicked collators should be slashed (set 0 to disable)
196        type SlashRatio: Get<Perbill>;
197
198        /// Used to check whether an account is allowed to be a candidate.
199        type AccountCheck: AccountCheck<Self::AccountId>;
200
201        /// The weight information of this pallet.
202        type WeightInfo: WeightInfo;
203    }
204
205    /// Basic information about a collation candidate.
206    #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)]
207    pub struct CandidateInfo<AccountId, Balance> {
208        /// Account identifier.
209        pub who: AccountId,
210        /// Reserved deposit.
211        pub deposit: Balance,
212    }
213
214    #[pallet::pallet]
215    #[pallet::without_storage_info]
216    pub struct Pallet<T>(_);
217
218    /// The invulnerable, fixed collators.
219    #[pallet::storage]
220    pub type Invulnerables<T: Config> = StorageValue<_, Vec<T::AccountId>, ValueQuery>;
221
222    /// The (community approved, limited) collation candidates.
223    #[pallet::storage]
224    pub type Candidates<T: Config> =
225        StorageValue<_, Vec<CandidateInfo<T::AccountId, BalanceOf<T>>>, ValueQuery>;
226
227    /// Candidates who initiated leave intent or kicked.
228    #[pallet::storage]
229    pub type NonCandidates<T: Config> =
230        StorageMap<_, Twox64Concat, T::AccountId, (SessionIndex, BalanceOf<T>), OptionQuery>;
231
232    /// Last block authored by collator.
233    #[pallet::storage]
234    pub type LastAuthoredBlock<T: Config> =
235        StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor<T>, ValueQuery>;
236
237    /// Desired number of candidates.
238    ///
239    /// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct.
240    #[pallet::storage]
241    pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;
242
243    /// Fixed amount to deposit to become a collator.
244    ///
245    /// When a collator calls `leave_intent` they immediately receive the deposit back.
246    #[pallet::storage]
247    pub type CandidacyBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;
248
249    /// Destination account for slashed amount.
250    #[pallet::storage]
251    pub type SlashDestination<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
252
253    /// Pending applications to become a collator.
254    #[pallet::storage]
255    pub type PendingApplications<T: Config> =
256        StorageMap<_, Twox64Concat, T::AccountId, BalanceOf<T>, OptionQuery>;
257
258    #[pallet::genesis_config]
259    #[derive(DefaultNoBound)]
260    pub struct GenesisConfig<T: Config> {
261        pub invulnerables: Vec<T::AccountId>,
262        pub candidacy_bond: BalanceOf<T>,
263        pub desired_candidates: u32,
264    }
265
266    #[pallet::genesis_build]
267    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
268        fn build(&self) {
269            let duplicate_invulnerables = self
270                .invulnerables
271                .iter()
272                .collect::<sp_std::collections::btree_set::BTreeSet<_>>();
273            assert_eq!(
274                duplicate_invulnerables.len(),
275                self.invulnerables.len(),
276                "duplicate invulnerables in genesis."
277            );
278
279            assert!(
280                T::MaxInvulnerables::get() >= (self.invulnerables.len() as u32),
281                "genesis invulnerables are more than T::MaxInvulnerables",
282            );
283            assert!(
284                T::MaxCandidates::get() >= self.desired_candidates,
285                "genesis desired_candidates are more than T::MaxCandidates",
286            );
287
288            <DesiredCandidates<T>>::put(self.desired_candidates);
289            <CandidacyBond<T>>::put(self.candidacy_bond);
290            <Invulnerables<T>>::put(&self.invulnerables);
291        }
292    }
293
294    #[pallet::event]
295    #[pallet::generate_deposit(pub(super) fn deposit_event)]
296    pub enum Event<T: Config> {
297        /// New invulnerables candidates were set.
298        NewInvulnerables(Vec<T::AccountId>),
299        /// The number of desired candidates was set.
300        NewDesiredCandidates(u32),
301        /// The candidacy bond was set.
302        NewCandidacyBond(BalanceOf<T>),
303        /// A new candidate joined.
304        CandidateAdded(T::AccountId, BalanceOf<T>),
305        /// A candidate was removed.
306        CandidateRemoved(T::AccountId),
307        /// A candidate was slashed.
308        CandidateSlashed(T::AccountId),
309        /// A new candidacy application was submitted.
310        CandidacyApplicationSubmitted(T::AccountId, BalanceOf<T>),
311        /// A candidacy application was approved.
312        CandidacyApplicationApproved(T::AccountId, BalanceOf<T>),
313        /// A candidacy application was closed.
314        CandidacyApplicationClosed(T::AccountId),
315        /// A candidate was kicked.
316        CandidateKicked(T::AccountId),
317    }
318
319    // Errors inform users that something went wrong.
320    #[pallet::error]
321    pub enum Error<T> {
322        /// Too many candidates
323        TooManyCandidates,
324        /// Too few candidates
325        TooFewCandidates,
326        /// Unknown error
327        Unknown,
328        /// Permission issue
329        Permission,
330        /// User is already a candidate
331        AlreadyCandidate,
332        /// User is not a candidate
333        NotCandidate,
334        /// User is already an Invulnerable
335        AlreadyInvulnerable,
336        /// User is not an Invulnerable
337        NotInvulnerable,
338        /// Account has no associated validator ID
339        NoAssociatedValidatorId,
340        /// Validator ID is not yet registered
341        ValidatorNotRegistered,
342        /// Account is now allowed to be a candidate due to an external reason (e.g. it might be participating in dApp staking)
343        NotAllowedCandidate,
344        /// The candidacy bond is currently in the un-bonding period.
345        BondStillLocked,
346        /// No candidacy bond available for withdrawal.
347        NoCandidacyBond,
348        /// User has already submitted an application
349        PendingApplicationExists,
350        /// No candidacy application found
351        NoApplicationFound,
352    }
353
354    #[pallet::hooks]
355    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
356
357    #[pallet::call]
358    impl<T: Config> Pallet<T> {
359        /// Set the list of invulnerable (fixed) collators.
360        #[pallet::call_index(0)]
361        #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]
362        pub fn set_invulnerables(
363            origin: OriginFor<T>,
364            new: Vec<T::AccountId>,
365        ) -> DispatchResultWithPostInfo {
366            T::UpdateOrigin::ensure_origin(origin)?;
367            // we trust origin calls, this is just a for more accurate benchmarking
368            if (new.len() as u32) > T::MaxInvulnerables::get() {
369                log::warn!(
370                    "invulnerables > T::MaxInvulnerables; you might need to run benchmarks again"
371                );
372            }
373
374            // check if the invulnerables have associated validator keys before they are set
375            for account_id in &new {
376                Self::is_validator_registered(account_id)?;
377            }
378
379            <Invulnerables<T>>::put(&new);
380            Self::deposit_event(Event::NewInvulnerables(new));
381            Ok(().into())
382        }
383
384        /// Set the ideal number of collators (not including the invulnerables).
385        /// If lowering this number, then the number of running collators could be higher than this figure.
386        /// Aside from that edge case, there should be no other way to have more collators than the desired number.
387        #[pallet::call_index(1)]
388        #[pallet::weight(T::WeightInfo::set_desired_candidates())]
389        pub fn set_desired_candidates(
390            origin: OriginFor<T>,
391            max: u32,
392        ) -> DispatchResultWithPostInfo {
393            T::UpdateOrigin::ensure_origin(origin)?;
394            // we trust origin calls, this is just a for more accurate benchmarking
395            if max > T::MaxCandidates::get() {
396                log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");
397            }
398            <DesiredCandidates<T>>::put(max);
399            Self::deposit_event(Event::NewDesiredCandidates(max));
400            Ok(().into())
401        }
402
403        /// Set the candidacy bond amount.
404        #[pallet::call_index(2)]
405        #[pallet::weight(T::WeightInfo::set_candidacy_bond())]
406        pub fn set_candidacy_bond(
407            origin: OriginFor<T>,
408            bond: BalanceOf<T>,
409        ) -> DispatchResultWithPostInfo {
410            T::UpdateOrigin::ensure_origin(origin)?;
411            <CandidacyBond<T>>::put(bond);
412            Self::deposit_event(Event::NewCandidacyBond(bond));
413            Ok(().into())
414        }
415
416        /// Register this account as a collator candidate. The account must (a) already have
417        /// registered session keys and (b) be able to reserve the `CandidacyBond`.
418        ///
419        /// **DEPRECATED**: This extrinsic is deprecated and will be removed in a future version.
420        /// Applications are now automatically processed when approved via `accept_application`.
421        /// This function now always fails to enforce the new permissioned workflow.
422        ///
423        /// This call is not available to `Invulnerable` collators.
424        #[pallet::call_index(3)]
425        #[pallet::weight(T::WeightInfo::register_as_candidate())]
426        pub fn register_as_candidate(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
427            let _who = ensure_signed(origin)?;
428            // Always fail with permission error to enforce new workflow
429            Err(Error::<T>::Permission.into())
430        }
431
432        /// Deregister `origin` as a collator candidate. Note that the collator can only leave on
433        /// session change. The `CandidacyBond` will start un-bonding process.
434        ///
435        /// This call will fail if the total number of candidates would drop below `MinCandidates`.
436        ///
437        /// This call is not available to `Invulnerable` collators.
438        #[pallet::call_index(4)]
439        #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]
440        pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
441            let who = ensure_signed(origin)?;
442            ensure!(
443                Candidates::<T>::get().len() as u32 > T::MinCandidates::get(),
444                Error::<T>::TooFewCandidates
445            );
446            let current_count = Self::try_remove_candidate(&who)?;
447            Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
448        }
449
450        /// Withdraw `CandidacyBond` after un-bonding period has finished.
451        /// This call will fail called during un-bonding or if there's no `CandidacyBound` reserved.
452        #[pallet::call_index(5)]
453        #[pallet::weight(T::WeightInfo::withdraw_bond())]
454        pub fn withdraw_bond(origin: OriginFor<T>) -> DispatchResult {
455            let who = ensure_signed(origin)?;
456
457            <NonCandidates<T>>::try_mutate_exists(&who, |maybe| -> DispatchResult {
458                if let Some((index, deposit)) = maybe.take() {
459                    ensure!(
460                        T::ValidatorSet::session_index() >= index,
461                        Error::<T>::BondStillLocked
462                    );
463                    T::Currency::unreserve(&who, deposit);
464                    <LastAuthoredBlock<T>>::remove(&who);
465                    Ok(())
466                } else {
467                    Err(Error::<T>::NoCandidacyBond.into())
468                }
469            })?;
470
471            Ok(())
472        }
473
474        /// Set slash destination.
475        /// Use `Some` to deposit slashed balance into destination or `None` to burn it.
476        #[pallet::call_index(6)]
477        #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
478        pub fn set_slash_destination(
479            origin: OriginFor<T>,
480            destination: Option<T::AccountId>,
481        ) -> DispatchResult {
482            T::UpdateOrigin::ensure_origin(origin)?;
483            match destination {
484                Some(account) => <SlashDestination<T>>::put(account),
485                None => <SlashDestination<T>>::kill(),
486            }
487            Ok(())
488        }
489
490        /// Add an invulnerable collator.
491        #[pallet::call_index(7)]
492        #[pallet::weight(T::WeightInfo::add_invulnerable(T::MaxInvulnerables::get()))]
493        pub fn add_invulnerable(
494            origin: OriginFor<T>,
495            who: T::AccountId,
496        ) -> DispatchResultWithPostInfo {
497            T::UpdateOrigin::ensure_origin(origin)?;
498            Self::is_validator_registered(&who)?;
499
500            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
501                ensure!(
502                    !invulnerables.contains(&who),
503                    Error::<T>::AlreadyInvulnerable
504                );
505                invulnerables.push(who);
506                Ok(())
507            })?;
508
509            Self::deposit_event(Event::NewInvulnerables(<Invulnerables<T>>::get()));
510            Ok(().into())
511        }
512
513        /// Remove an invulnerable collator.
514        #[pallet::call_index(8)]
515        #[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxInvulnerables::get()))]
516        pub fn remove_invulnerable(
517            origin: OriginFor<T>,
518            who: T::AccountId,
519        ) -> DispatchResultWithPostInfo {
520            T::UpdateOrigin::ensure_origin(origin)?;
521            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
522                if let Some(pos) = invulnerables.iter().position(|acc| *acc == who) {
523                    invulnerables.remove(pos);
524                    Ok(())
525                } else {
526                    Err(Error::<T>::NotInvulnerable.into())
527                }
528            })?;
529
530            Self::deposit_event(Event::NewInvulnerables(<Invulnerables<T>>::get()));
531            Ok(().into())
532        }
533
534        /// Submit an application to become a collator candidate. The account must already have
535        /// registered session keys.
536        ///
537        /// This call is not available to `Invulnerable` collators or exisiting candidates.
538        #[pallet::call_index(9)]
539        #[pallet::weight(T::WeightInfo::apply_for_candidacy())]
540        pub fn apply_for_candidacy(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
541            let who = ensure_signed(origin)?;
542
543            ensure!(
544                !PendingApplications::<T>::contains_key(&who),
545                Error::<T>::PendingApplicationExists
546            );
547            ensure!(
548                !Invulnerables::<T>::get().contains(&who),
549                Error::<T>::AlreadyInvulnerable
550            );
551            ensure!(
552                T::AccountCheck::allowed_candidacy(&who),
553                Error::<T>::NotAllowedCandidate
554            );
555            ensure!(
556                !Self::is_account_candidate(&who),
557                Error::<T>::AlreadyCandidate
558            );
559
560            Self::is_validator_registered(&who)?;
561
562            // ensure candidacy has no previous locked un-bonding
563            <NonCandidates<T>>::try_mutate_exists(&who, |maybe| -> DispatchResult {
564                if let Some((index, deposit)) = maybe.take() {
565                    ensure!(
566                        T::ValidatorSet::session_index() >= index,
567                        Error::<T>::BondStillLocked
568                    );
569                    // unreserve previous deposit and continue with registration
570                    T::Currency::unreserve(&who, deposit);
571                }
572                Ok(())
573            })?;
574
575            let bond = CandidacyBond::<T>::get();
576            T::Currency::reserve(&who, bond)?;
577            PendingApplications::<T>::insert(&who, bond);
578
579            Self::deposit_event(Event::CandidacyApplicationSubmitted(who, bond));
580            Ok(().into())
581        }
582
583        /// Close a pending candidacy application and unreserve the bond.
584        ///
585        /// Can only be called by the account that submitted the application or
586        /// by governance origin.
587        #[pallet::call_index(10)]
588        #[pallet::weight(T::WeightInfo::close_application())]
589        pub fn close_application(
590            origin: OriginFor<T>,
591            who: T::AccountId,
592        ) -> DispatchResultWithPostInfo {
593            Self::ensure_governance_or_caller(origin, &who)?;
594
595            let deposit =
596                PendingApplications::<T>::take(&who).ok_or(Error::<T>::NoApplicationFound)?;
597
598            T::Currency::unreserve(&who, deposit);
599
600            Self::deposit_event(Event::CandidacyApplicationClosed(who));
601            Ok(().into())
602        }
603
604        /// Approve a pending candidacy application.
605        ///
606        /// This will remove the application from pending status and immediately add the account
607        /// to the candidates list, making them eligible for collator selection.
608        #[pallet::call_index(11)]
609        #[pallet::weight(T::WeightInfo::approve_application(T::MaxCandidates::get()))]
610        pub fn approve_application(
611            origin: OriginFor<T>,
612            who: T::AccountId,
613        ) -> DispatchResultWithPostInfo {
614            T::GovernanceOrigin::ensure_origin(origin)?;
615
616            let deposit =
617                PendingApplications::<T>::take(&who).ok_or(Error::<T>::NoApplicationFound)?;
618
619            ensure!(
620                T::AccountCheck::allowed_candidacy(&who),
621                Error::<T>::NotAllowedCandidate
622            );
623
624            // Check candidate limits
625            let current_candidates = Candidates::<T>::decode_len().unwrap_or_default() as u32;
626            ensure!(
627                current_candidates < DesiredCandidates::<T>::get(),
628                Error::<T>::TooManyCandidates
629            );
630
631            Self::is_validator_registered(&who)?;
632
633            let incoming = CandidateInfo {
634                who: who.clone(),
635                deposit,
636            };
637
638            let current_count = <Candidates<T>>::mutate(|candidates| {
639                candidates.push(incoming);
640                <LastAuthoredBlock<T>>::insert(
641                    &who,
642                    // first authored block is current block plus kick threshold to handle session delay
643                    frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),
644                );
645                candidates.len()
646            });
647
648            Self::deposit_event(Event::CandidateAdded(who, deposit));
649            Ok(Some(T::WeightInfo::approve_application(current_count as u32)).into())
650        }
651
652        /// Forcibly remove a candidate from the active set.
653        ///
654        /// This will immediately remove the candidate from the candidates list and
655        /// unbond their deposit after slashing it accordigly to `SlashRatio`.
656        ///
657        /// This call will fail if removing the candidate would bring the total
658        /// number of candidates below the minimum threshold.
659        #[pallet::call_index(12)]
660        #[pallet::weight(T::WeightInfo::kick_candidate(T::MaxInvulnerables::get()))]
661        pub fn kick_candidate(
662            origin: OriginFor<T>,
663            who: T::AccountId,
664        ) -> DispatchResultWithPostInfo {
665            T::ForceRemovalOrigin::ensure_origin(origin)?;
666            ensure!(
667                Candidates::<T>::get().len() > T::MinCandidates::get() as usize,
668                Error::<T>::TooFewCandidates
669            );
670            let current_count = Self::try_remove_candidate(&who)?;
671            // slashed funds are released immediately, this is safe since it can only be
672            // called by force origin.
673            Self::slash_non_candidate(&who);
674
675            Self::deposit_event(Event::CandidateKicked(who));
676            Ok(Some(T::WeightInfo::kick_candidate(current_count as u32)).into())
677        }
678    }
679
680    impl<T: Config> Pallet<T> {
681        /// Get a unique, inaccessible account id from the `PotId`.
682        pub fn account_id() -> T::AccountId {
683            T::PotId::get().into_account_truncating()
684        }
685
686        fn ensure_governance_or_caller(
687            origin: T::RuntimeOrigin,
688            who: &T::AccountId,
689        ) -> DispatchResult {
690            if T::GovernanceOrigin::ensure_origin(origin.clone()).is_err() {
691                let caller = ensure_signed(origin.clone())?;
692                ensure!(&caller == who, BadOrigin);
693            }
694            Ok(())
695        }
696
697        /// Checks if the account has a registered validator key.
698        fn is_validator_registered(who: &T::AccountId) -> DispatchResult {
699            let validator_key = T::ValidatorIdOf::convert(who.clone())
700                .ok_or(Error::<T>::NoAssociatedValidatorId)?;
701            ensure!(
702                T::ValidatorRegistration::is_registered(&validator_key),
703                Error::<T>::ValidatorNotRegistered
704            );
705
706            Ok(())
707        }
708
709        /// Removes a candidate if they exist. Start deposit un-bonding
710        fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {
711            let current_count =
712                <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {
713                    let index = candidates
714                        .iter()
715                        .position(|candidate| candidate.who == *who)
716                        .ok_or(Error::<T>::NotCandidate)?;
717
718                    let candidate = candidates.remove(index);
719                    // start un-bonding period, 1 session
720                    let session_index = T::ValidatorSet::session_index().saturating_add(1);
721                    <NonCandidates<T>>::insert(who, (session_index, candidate.deposit));
722                    Ok(candidates.len())
723                })?;
724            Self::deposit_event(Event::CandidateRemoved(who.clone()));
725            Ok(current_count)
726        }
727
728        /// Slash candidate deposit and return the rest of funds.
729        /// NOTE: The slashed bond is released immediately here without unbonding.
730        /// This is intentional as long as unbonding period is less than kick threshold,
731        /// this is safe. If this changes in the future, we need to revisit this logic.
732        fn slash_non_candidate(who: &T::AccountId) {
733            NonCandidates::<T>::mutate_exists(who, |maybe| {
734                if let Some((_index, deposit)) = maybe.take() {
735                    let slash = T::SlashRatio::get() * deposit;
736                    let remain = deposit.saturating_sub(slash);
737
738                    let (imbalance, _) = T::Currency::slash_reserved(who, slash);
739                    T::Currency::unreserve(who, remain);
740
741                    if let Some(dest) = SlashDestination::<T>::get() {
742                        T::Currency::resolve_creating(&dest, imbalance);
743                    }
744
745                    <LastAuthoredBlock<T>>::remove(who);
746
747                    Self::deposit_event(Event::CandidateSlashed(who.clone()));
748                }
749            });
750        }
751
752        /// Assemble the current set of candidates and invulnerables into the next collator set.
753        ///
754        /// This is done on the fly, as frequent as we are told to do so, as the session manager.
755        pub fn assemble_collators(candidates: Vec<T::AccountId>) -> Vec<T::AccountId> {
756            let mut collators = Invulnerables::<T>::get();
757            collators.extend(candidates.into_iter());
758            collators
759        }
760        /// Kicks out and candidates that did not produce a block in the kick threshold.
761        /// Return length of candidates before and number of kicked candidates.
762        pub fn kick_stale_candidates() -> (u32, u32) {
763            let now = frame_system::Pallet::<T>::block_number();
764            let kick_threshold = T::KickThreshold::get();
765            let count = Candidates::<T>::get().len() as u32;
766            for (who, last_authored) in LastAuthoredBlock::<T>::iter() {
767                if now.saturating_sub(last_authored) < kick_threshold {
768                    continue;
769                }
770                // stale candidate, kick and slash
771                if Self::is_account_candidate(&who) {
772                    if Candidates::<T>::get().len() > T::MinCandidates::get() as usize {
773                        // no error, who is a candidate
774                        let _ = Self::try_remove_candidate(&who);
775                        Self::slash_non_candidate(&who);
776                    }
777                } else if let Some((locked_until, _)) = NonCandidates::<T>::get(&who) {
778                    if T::ValidatorSet::session_index() > locked_until {
779                        // bond is already unlocked
780                        <LastAuthoredBlock<T>>::remove(who);
781                    } else {
782                        // slash un-bonding candidate
783                        Self::slash_non_candidate(&who);
784                    }
785                }
786            }
787            (
788                count,
789                count.saturating_sub(Candidates::<T>::get().len() as u32),
790            )
791        }
792
793        /// Check whether an account is a candidate.
794        pub fn is_account_candidate(account: &T::AccountId) -> bool {
795            Candidates::<T>::get().iter().any(|c| &c.who == account)
796        }
797    }
798
799    /// Keep track of number of authored blocks per authority, uncles are counted as well since
800    /// they're a valid proof of being online.
801    impl<T: Config + pallet_authorship::Config>
802        pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T>
803    {
804        fn note_author(author: T::AccountId) {
805            let pot = Self::account_id();
806            // assumes an ED will be sent to pot.
807            let reward = T::Currency::free_balance(&pot)
808                .checked_sub(&T::Currency::minimum_balance())
809                .unwrap_or_else(Zero::zero)
810                .div(2u32.into());
811            // `reward` is half of pot account minus ED, this should never fail.
812            let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);
813            debug_assert!(_success.is_ok());
814            <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());
815
816            frame_system::Pallet::<T>::register_extra_weight_unchecked(
817                T::WeightInfo::note_author(),
818                DispatchClass::Mandatory,
819            );
820        }
821    }
822
823    /// Play the role of the session manager.
824    impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {
825        fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {
826            log::info!(
827                "assembling new collators for new session {} at #{:?}",
828                index,
829                <frame_system::Pallet<T>>::block_number(),
830            );
831
832            let (candidates_len_before, removed) = Self::kick_stale_candidates();
833            frame_system::Pallet::<T>::register_extra_weight_unchecked(
834                T::WeightInfo::new_session(candidates_len_before, removed),
835                DispatchClass::Mandatory,
836            );
837
838            let active_candidates = Candidates::<T>::get()
839                .into_iter()
840                .map(|x| x.who)
841                .collect::<Vec<_>>();
842
843            Some(Self::assemble_collators(active_candidates))
844        }
845        fn start_session(_: SessionIndex) {
846            // we don't care.
847        }
848        fn end_session(_: SessionIndex) {
849            // we don't care.
850        }
851    }
852}