// Copyright (C) 2021 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Collator Selection pallet. //! //! A pallet to manage collators in a parachain. //! //! ## Overview //! //! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a //! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT //! safety assumptions of the chosen set. //! //! ## Terminology //! //! - Collator: A parachain block producer. //! - Bond: An amount of `Balance` _reserved_ for candidate registration. //! - Invulnerable: An account guaranteed to be in the collator set. //! //! ## Implementation //! //! The final `Collators` are aggregated from two individual lists: //! //! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be //! collators. //! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as //! a final collator. //! //! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve //! manner. //! //! Candidates will not be allowed to get kicked or `leave_intent` if the total number of collators //! would fall below `MinEligibleCollators`. This is to ensure that some collators will always //! exist, i.e. someone is eligible to produce a block. //! //! ### Rewards //! //! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the //! collator who authored it receives: //! //! - Half the value of the Pot. //! - Half the value of the transaction fees within the block. The other half of the transaction //! fees are deposited into the Pot. //! //! To initiate rewards, an ED needs to be transferred to the pot address. //! //! Note: Eventually the Pot distribution may be modified as discussed in //! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073). #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; #[cfg(test)] mod mock; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod migration; pub mod weights; const LOG_TARGET: &str = "runtime::collator-selection"; #[frame_support::pallet] pub mod pallet { pub use crate::weights::WeightInfo; use core::ops::Div; use frame_support::{ dispatch::{DispatchClass, DispatchResultWithPostInfo}, pallet_prelude::*, traits::{ Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency, ValidatorRegistration, }, BoundedVec, DefaultNoBound, PalletId, }; use frame_system::{pallet_prelude::*, Config as SystemConfig}; use pallet_session::SessionManager; use sp_runtime::{ traits::{AccountIdConversion, CheckedSub, Convert, Saturating, Zero}, RuntimeDebug, }; use sp_staking::SessionIndex; use sp_std::vec::Vec; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; /// A convertor from collators id. Since this pallet does not have stash/controller, this is /// just identity. pub struct IdentityCollator; impl sp_runtime::traits::Convert> for IdentityCollator { fn convert(t: T) -> Option { Some(t) } } /// Configure the pallet by specifying the parameters and types on which it depends. #[pallet::config] pub trait Config: frame_system::Config { /// Overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The currency mechanism. type Currency: ReservableCurrency; /// Origin that can dictate updating parameters of this pallet. type UpdateOrigin: EnsureOrigin; /// Account Identifier from which the internal Pot is generated. type PotId: Get; /// Maximum number of candidates that we should have. /// /// This does not take into account the invulnerables. type MaxCandidates: Get; /// Minimum number eligible collators. Should always be greater than zero. This includes /// Invulnerable collators. This ensures that there will always be one collator who can /// produce a block. type MinEligibleCollators: Get; /// Maximum number of invulnerables. type MaxInvulnerables: Get; // Will be kicked if block is not produced in threshold. type KickThreshold: Get>; /// A stable ID for a validator. type ValidatorId: Member + Parameter; /// A conversion from account ID to validator ID. /// /// Its cost must be at most one storage read. type ValidatorIdOf: Convert>; /// Validate a user is registered type ValidatorRegistration: ValidatorRegistration; /// The weight information of this pallet. type WeightInfo: WeightInfo; } /// Basic information about a collation candidate. #[derive( PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, )] pub struct CandidateInfo { /// Account identifier. pub who: AccountId, /// Reserved deposit. pub deposit: Balance, } #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); /// The invulnerable, permissioned collators. This list must be sorted. #[pallet::storage] #[pallet::getter(fn invulnerables)] pub type Invulnerables = StorageValue<_, BoundedVec, ValueQuery>; /// The (community, limited) collation candidates. `Candidates` and `Invulnerables` should be /// mutually exclusive. #[pallet::storage] #[pallet::getter(fn candidates)] pub type Candidates = StorageValue< _, BoundedVec>, T::MaxCandidates>, ValueQuery, >; /// Last block authored by collator. #[pallet::storage] #[pallet::getter(fn last_authored_block)] pub type LastAuthoredBlock = StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor, ValueQuery>; /// Desired number of candidates. /// /// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct. #[pallet::storage] #[pallet::getter(fn desired_candidates)] pub type DesiredCandidates = StorageValue<_, u32, ValueQuery>; /// Fixed amount to deposit to become a collator. /// /// When a collator calls `leave_intent` they immediately receive the deposit back. #[pallet::storage] #[pallet::getter(fn candidacy_bond)] pub type CandidacyBond = StorageValue<_, BalanceOf, ValueQuery>; #[pallet::genesis_config] #[derive(DefaultNoBound)] pub struct GenesisConfig { pub invulnerables: Vec, pub candidacy_bond: BalanceOf, pub desired_candidates: u32, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { let duplicate_invulnerables = self .invulnerables .iter() .collect::>(); assert!( duplicate_invulnerables.len() == self.invulnerables.len(), "duplicate invulnerables in genesis." ); let mut bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone()) .expect("genesis invulnerables are more than T::MaxInvulnerables"); assert!( T::MaxCandidates::get() >= self.desired_candidates, "genesis desired_candidates are more than T::MaxCandidates", ); bounded_invulnerables.sort(); >::put(self.desired_candidates); >::put(self.candidacy_bond); >::put(bounded_invulnerables); } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// New Invulnerables were set. NewInvulnerables { invulnerables: Vec }, /// A new Invulnerable was added. InvulnerableAdded { account_id: T::AccountId }, /// An Invulnerable was removed. InvulnerableRemoved { account_id: T::AccountId }, /// The number of desired candidates was set. NewDesiredCandidates { desired_candidates: u32 }, /// The candidacy bond was set. NewCandidacyBond { bond_amount: BalanceOf }, /// A new candidate joined. CandidateAdded { account_id: T::AccountId, deposit: BalanceOf }, /// A candidate was removed. CandidateRemoved { account_id: T::AccountId }, /// An account was unable to be added to the Invulnerables because they did not have keys /// registered. Other Invulnerables may have been set. InvalidInvulnerableSkipped { account_id: T::AccountId }, } #[pallet::error] pub enum Error { /// The pallet has too many candidates. TooManyCandidates, /// Leaving would result in too few candidates. TooFewEligibleCollators, /// Account is already a candidate. AlreadyCandidate, /// Account is not a candidate. NotCandidate, /// There are too many Invulnerables. TooManyInvulnerables, /// Account is already an Invulnerable. AlreadyInvulnerable, /// Account is not an Invulnerable. NotInvulnerable, /// Account has no associated validator ID. NoAssociatedValidatorId, /// Validator ID is not yet registered. ValidatorNotRegistered, } #[pallet::hooks] impl Hooks> for Pallet { fn integrity_test() { assert!(T::MinEligibleCollators::get() > 0, "chain must require at least one collator"); } } #[pallet::call] impl Pallet { /// Set the list of invulnerable (fixed) collators. These collators must do some /// preparation, namely to have registered session keys. /// /// The call will remove any accounts that have not registered keys from the set. That is, /// it is non-atomic; the caller accepts all `AccountId`s passed in `new` _individually_ as /// acceptable Invulnerables, and is not proposing a _set_ of new Invulnerables. /// /// This call does not maintain mutual exclusivity of `Invulnerables` and `Candidates`. It /// is recommended to use a batch of `add_invulnerable` and `remove_invulnerable` instead. /// A `batch_all` can also be used to enforce atomicity. If any candidates are included in /// `new`, they should be removed with `remove_invulnerable_candidate` after execution. /// /// Must be called by the `UpdateOrigin`. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))] pub fn set_invulnerables(origin: OriginFor, new: Vec) -> DispatchResult { T::UpdateOrigin::ensure_origin(origin)?; // don't wipe out the collator set if new.is_empty() { ensure!( Candidates::::decode_len().unwrap_or_default() >= T::MinEligibleCollators::get() as usize, Error::::TooFewEligibleCollators ); } // Will need to check the length again when putting into a bounded vec, but this // prevents the iterator from having too many elements. ensure!( new.len() as u32 <= T::MaxInvulnerables::get(), Error::::TooManyInvulnerables ); let mut new_with_keys = Vec::new(); // check if the invulnerables have associated validator keys before they are set for account_id in &new { // don't let one unprepared collator ruin things for everyone. let validator_key = T::ValidatorIdOf::convert(account_id.clone()); match validator_key { Some(key) => { // key is not registered if !T::ValidatorRegistration::is_registered(&key) { Self::deposit_event(Event::InvalidInvulnerableSkipped { account_id: account_id.clone(), }); continue } // else condition passes; key is registered }, // key does not exist None => { Self::deposit_event(Event::InvalidInvulnerableSkipped { account_id: account_id.clone(), }); continue }, } new_with_keys.push(account_id.clone()); } // should never fail since `new_with_keys` must be equal to or shorter than `new` let mut bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new_with_keys) .map_err(|_| Error::::TooManyInvulnerables)?; // Invulnerables must be sorted for removal. bounded_invulnerables.sort(); >::put(&bounded_invulnerables); Self::deposit_event(Event::NewInvulnerables { invulnerables: bounded_invulnerables.to_vec(), }); Ok(()) } /// Set the ideal number of non-invulnerable collators. If lowering this number, then the /// number of running collators could be higher than this figure. Aside from that edge case, /// there should be no other way to have more candidates than the desired number. /// /// The origin for this call must be the `UpdateOrigin`. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::set_desired_candidates())] pub fn set_desired_candidates( origin: OriginFor, max: u32, ) -> DispatchResultWithPostInfo { T::UpdateOrigin::ensure_origin(origin)?; // we trust origin calls, this is just a for more accurate benchmarking if max > T::MaxCandidates::get() { log::warn!("max > T::MaxCandidates; you might need to run benchmarks again"); } >::put(max); Self::deposit_event(Event::NewDesiredCandidates { desired_candidates: max }); Ok(().into()) } /// Set the candidacy bond amount. /// /// The origin for this call must be the `UpdateOrigin`. #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::set_candidacy_bond())] pub fn set_candidacy_bond( origin: OriginFor, bond: BalanceOf, ) -> DispatchResultWithPostInfo { T::UpdateOrigin::ensure_origin(origin)?; >::put(bond); Self::deposit_event(Event::NewCandidacyBond { bond_amount: bond }); Ok(().into()) } /// Register this account as a collator candidate. The account must (a) already have /// registered session keys and (b) be able to reserve the `CandidacyBond`. /// /// This call is not available to `Invulnerable` collators. #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] pub fn register_as_candidate(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; // ensure we are below limit. let length = >::decode_len().unwrap_or_default(); ensure!((length as u32) < Self::desired_candidates(), Error::::TooManyCandidates); ensure!(!Self::invulnerables().contains(&who), Error::::AlreadyInvulnerable); let validator_key = T::ValidatorIdOf::convert(who.clone()) .ok_or(Error::::NoAssociatedValidatorId)?; ensure!( T::ValidatorRegistration::is_registered(&validator_key), Error::::ValidatorNotRegistered ); let deposit = Self::candidacy_bond(); // First authored block is current block plus kick threshold to handle session delay let incoming = CandidateInfo { who: who.clone(), deposit }; let current_count = >::try_mutate(|candidates| -> Result { if candidates.iter().any(|candidate| candidate.who == who) { Err(Error::::AlreadyCandidate)? } else { T::Currency::reserve(&who, deposit)?; candidates.try_push(incoming).map_err(|_| Error::::TooManyCandidates)?; >::insert( who.clone(), frame_system::Pallet::::block_number() + T::KickThreshold::get(), ); Ok(candidates.len()) } })?; Self::deposit_event(Event::CandidateAdded { account_id: who, deposit }); Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into()) } /// Deregister `origin` as a collator candidate. Note that the collator can only leave on /// session change. The `CandidacyBond` will be unreserved immediately. /// /// This call will fail if the total number of candidates would drop below /// `MinEligibleCollators`. #[pallet::call_index(4)] #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] pub fn leave_intent(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; ensure!( Self::eligible_collators() > T::MinEligibleCollators::get() as usize, Error::::TooFewEligibleCollators ); // Do remove their last authored block. let current_count = Self::try_remove_candidate(&who, true)?; Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) } /// Add a new account `who` to the list of `Invulnerables` collators. `who` must have /// registered session keys. If `who` is a candidate, they will be removed. /// /// The origin for this call must be the `UpdateOrigin`. #[pallet::call_index(5)] #[pallet::weight(T::WeightInfo::add_invulnerable( T::MaxInvulnerables::get().saturating_sub(1), T::MaxCandidates::get() ))] pub fn add_invulnerable( origin: OriginFor, who: T::AccountId, ) -> DispatchResultWithPostInfo { T::UpdateOrigin::ensure_origin(origin)?; // ensure `who` has registered a validator key let validator_key = T::ValidatorIdOf::convert(who.clone()) .ok_or(Error::::NoAssociatedValidatorId)?; ensure!( T::ValidatorRegistration::is_registered(&validator_key), Error::::ValidatorNotRegistered ); >::try_mutate(|invulnerables| -> DispatchResult { match invulnerables.binary_search(&who) { Ok(_) => return Err(Error::::AlreadyInvulnerable)?, Err(pos) => invulnerables .try_insert(pos, who.clone()) .map_err(|_| Error::::TooManyInvulnerables)?, } Ok(()) })?; // Error just means `who` wasn't a candidate, which is the state we want anyway. Don't // remove their last authored block, as they are still a collator. let _ = Self::try_remove_candidate(&who, false); Self::deposit_event(Event::InvulnerableAdded { account_id: who }); let weight_used = T::WeightInfo::add_invulnerable( Invulnerables::::decode_len() .unwrap_or_default() .try_into() .unwrap_or(T::MaxInvulnerables::get().saturating_sub(1)), Candidates::::decode_len() .unwrap_or_default() .try_into() .unwrap_or(T::MaxCandidates::get()), ); Ok(Some(weight_used).into()) } /// Remove an account `who` from the list of `Invulnerables` collators. `Invulnerables` must /// be sorted. /// /// The origin for this call must be the `UpdateOrigin`. #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxInvulnerables::get()))] pub fn remove_invulnerable(origin: OriginFor, who: T::AccountId) -> DispatchResult { T::UpdateOrigin::ensure_origin(origin)?; ensure!( Self::eligible_collators() > T::MinEligibleCollators::get() as usize, Error::::TooFewEligibleCollators ); >::try_mutate(|invulnerables| -> DispatchResult { let pos = invulnerables.binary_search(&who).map_err(|_| Error::::NotInvulnerable)?; invulnerables.remove(pos); Ok(()) })?; Self::deposit_event(Event::InvulnerableRemoved { account_id: who }); Ok(()) } } impl Pallet { /// Get a unique, inaccessible account ID from the `PotId`. pub fn account_id() -> T::AccountId { T::PotId::get().into_account_truncating() } /// Return the total number of accounts that are eligible collators (candidates and /// invulnerables). fn eligible_collators() -> usize { Candidates::::decode_len() .unwrap_or_default() .saturating_add(Invulnerables::::decode_len().unwrap_or_default()) } /// Removes a candidate if they exist and sends them back their deposit. fn try_remove_candidate( who: &T::AccountId, remove_last_authored: bool, ) -> Result { let current_count = >::try_mutate(|candidates| -> Result { let index = candidates .iter() .position(|candidate| candidate.who == *who) .ok_or(Error::::NotCandidate)?; let candidate = candidates.remove(index); T::Currency::unreserve(who, candidate.deposit); if remove_last_authored { >::remove(who.clone()) }; Ok(candidates.len()) })?; Self::deposit_event(Event::CandidateRemoved { account_id: who.clone() }); Ok(current_count) } /// Assemble the current set of candidates and invulnerables into the next collator set. /// /// This is done on the fly, as frequent as we are told to do so, as the session manager. pub fn assemble_collators( candidates: BoundedVec, ) -> Vec { let mut collators = Self::invulnerables().to_vec(); collators.extend(candidates); collators } /// Kicks out candidates that did not produce a block in the kick threshold and refunds /// their deposits. pub fn kick_stale_candidates( candidates: BoundedVec>, T::MaxCandidates>, ) -> BoundedVec { let now = frame_system::Pallet::::block_number(); let kick_threshold = T::KickThreshold::get(); let min_collators = T::MinEligibleCollators::get(); candidates .into_iter() .filter_map(|c| { let last_block = >::get(c.who.clone()); let since_last = now.saturating_sub(last_block); let is_invulnerable = Self::invulnerables().contains(&c.who); let is_lazy = since_last >= kick_threshold; if is_invulnerable { // They are invulnerable. No reason for them to be in Candidates also. // We don't even care about the min collators here, because an Account // should not be a collator twice. let _ = Self::try_remove_candidate(&c.who, false); None } else { if Self::eligible_collators() <= min_collators as usize || !is_lazy { // Either this is a good collator (not lazy) or we are at the minimum // that the system needs. They get to stay. Some(c.who) } else { // This collator has not produced a block recently enough. Bye bye. let _ = Self::try_remove_candidate(&c.who, true); None } } }) .collect::>() .try_into() .expect("filter_map operation can't result in a bounded vec larger than its original; qed") } } /// Keep track of number of authored blocks per authority, uncles are counted as well since /// they're a valid proof of being online. impl pallet_authorship::EventHandler> for Pallet { fn note_author(author: T::AccountId) { let pot = Self::account_id(); // assumes an ED will be sent to pot. let reward = T::Currency::free_balance(&pot) .checked_sub(&T::Currency::minimum_balance()) .unwrap_or_else(Zero::zero) .div(2u32.into()); // `reward` is half of pot account minus ED, this should never fail. let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive); debug_assert!(_success.is_ok()); >::insert(author, frame_system::Pallet::::block_number()); frame_system::Pallet::::register_extra_weight_unchecked( T::WeightInfo::note_author(), DispatchClass::Mandatory, ); } } /// Play the role of the session manager. impl SessionManager for Pallet { fn new_session(index: SessionIndex) -> Option> { log::info!( "assembling new collators for new session {} at #{:?}", index, >::block_number(), ); let candidates = Self::candidates(); let candidates_len_before = candidates.len(); let active_candidates = Self::kick_stale_candidates(candidates); let removed = candidates_len_before - active_candidates.len(); let result = Self::assemble_collators(active_candidates); frame_system::Pallet::::register_extra_weight_unchecked( T::WeightInfo::new_session(candidates_len_before as u32, removed as u32), DispatchClass::Mandatory, ); Some(result) } fn start_session(_: SessionIndex) { // we don't care. } fn end_session(_: SessionIndex) { // we don't care. } } }