difftreelog
Update pallets/collator-selection/src/lib.rs
in: master
1 file changed
pallets/collator-selection/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// Original license:18// Copyright (C) 2021 Parity Technologies (UK) Ltd.19// SPDX-License-Identifier: Apache-2.02021// Licensed under the Apache License, Version 2.0 (the "License");22// you may not use this file except in compliance with the License.23// You may obtain a copy of the License at24//25// http://www.apache.org/licenses/LICENSE-2.026//27// Unless required by applicable law or agreed to in writing, software28// distributed under the License is distributed on an "AS IS" BASIS,29// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.30// See the License for the specific language governing permissions and31// limitations under the License.3233// todo:collator documentation34//! Collator Selection pallet.35//!36//! A pallet to manage collators in a parachain.37//!38//! ## Overview39//!40//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a41//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT42//! safety assumptions of the chosen set.43//!44//! ## Terminology45//!46//! - Collator: A parachain block producer.47//! - Bond: An amount of `Balance` _reserved_ for candidate registration.48//! - Invulnerable: An account guaranteed to be in the collator set.49//!50//! ## Implementation51//!52//! The final `Collators` are aggregated from two individual lists:53//!54//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be55//! collators.56//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as57//! a final collator.58//!59//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve60//! manner.61//!62//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates63//! fall below MinCandidates. This is for potential disaster recovery scenarios.64//!65//! ### Rewards66//!67//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the68//! collator who authored it receives:69//!70//! - Half the value of the Pot.71//! - Half the value of the transaction fees within the block. The other half of the transaction72//! fees are deposited into the Pot.73//!74//! To initiate rewards an ED needs to be transferred to the pot address.75//!76//! Note: Eventually the Pot distribution may be modified as discussed in77//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7879#![cfg_attr(not(feature = "std"), no_std)]8081pub use pallet::*;8283#[cfg(test)]84mod mock;8586#[cfg(test)]87mod tests;8889#[cfg(feature = "runtime-benchmarks")]90mod benchmarking;91pub mod weights;9293#[frame_support::pallet]94pub mod pallet {95 use super::*;96 pub use crate::weights::WeightInfo;97 use core::ops::Div;98 use frame_support::{99 dispatch::{DispatchClass, DispatchResultWithPostInfo},100 inherent::Vec,101 pallet_prelude::*,102 sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},103 traits::{104 EnsureOrigin,105 fungible::{Balanced, BalancedHold, Inspect, InspectHold, Mutate, MutateHold},106 ValidatorRegistration,107 tokens::{Precision, Preservation},108 },109 BoundedVec, PalletId,110 };111 use frame_system::pallet_prelude::*;112 use pallet_session::SessionManager;113 use sp_runtime::{Perbill, traits::Convert};114 use pallet_configuration::{115 CollatorSelectionDesiredCollatorsOverride as DesiredCollators,116 CollatorSelectionLicenseBondOverride as LicenseBond,117 CollatorSelectionKickThresholdOverride as KickThreshold, BalanceOf,118 };119 use sp_staking::SessionIndex;120121 /// A convertor from collators id. Since this pallet does not have stash/controller, this is122 /// just identity.123 pub struct IdentityCollator;124 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {125 fn convert(t: T) -> Option<T> {126 Some(t)127 }128 }129130 /// Configure the pallet by specifying the parameters and types on which it depends.131 #[pallet::config]132 pub trait Config: frame_system::Config + pallet_configuration::Config {133 /// Overarching event type.134 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;135136 /// Origin that can dictate updating parameters of this pallet.137 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;138139 /// Account Identifier that holds the chain's treasury.140 type TreasuryAccountId: Get<Self::AccountId>;141142 /// Account Identifier from which the internal Pot is generated.143 type PotId: Get<PalletId>;144145 /// Maximum number of candidates and invulnerables that we should have. This is enforced in code.146 type MaxCollators: Get<u32>;147148 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.149 type SlashRatio: Get<Perbill>;150151 /// A stable ID for a validator.152 type ValidatorId: Member + Parameter;153154 /// A conversion from account ID to validator ID.155 ///156 /// Its cost must be at most one storage read.157 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;158159 /// Validate a user is registered160 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;161162 /// The weight information of this pallet.163 type WeightInfo: WeightInfo;164165 #[pallet::constant]166 type LicenceBondIdentifier: Get<<<Self as pallet_configuration::Config>::Currency as InspectHold<Self::AccountId>>::Reason>;167 }168169 #[pallet::pallet]170 pub struct Pallet<T>(_);171172 /// The invulnerable, fixed collators.173 #[pallet::storage]174 #[pallet::getter(fn invulnerables)]175 pub type Invulnerables<T: Config> =176 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;177178 /// The (community) collation license holders.179 #[pallet::storage]180 #[pallet::getter(fn license_deposit_of)]181 pub type LicenseDepositOf<T: Config> =182 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;183184 /// The (community, limited) collation candidates.185 #[pallet::storage]186 #[pallet::getter(fn candidates)]187 pub type Candidates<T: Config> =188 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;189190 /// Last block authored by collator.191 #[pallet::storage]192 #[pallet::getter(fn last_authored_block)]193 pub type LastAuthoredBlock<T: Config> =194 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;195196 #[pallet::genesis_config]197 pub struct GenesisConfig<T: Config> {198 pub invulnerables: Vec<T::AccountId>,199 }200201 #[cfg(feature = "std")]202 impl<T: Config> Default for GenesisConfig<T> {203 fn default() -> Self {204 Self {205 invulnerables: Default::default(),206 }207 }208 }209210 #[pallet::genesis_build]211 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {212 fn build(&self) {213 let duplicate_invulnerables = self214 .invulnerables215 .iter()216 .collect::<std::collections::BTreeSet<_>>();217 assert!(218 duplicate_invulnerables.len() == self.invulnerables.len(),219 "duplicate invulnerables in genesis."220 );221222 let bounded_invulnerables =223 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())224 .expect("genesis invulnerables are more than T::MaxCollators");225226 <Invulnerables<T>>::put(bounded_invulnerables);227 }228 }229230 #[pallet::event]231 #[pallet::generate_deposit(pub(super) fn deposit_event)]232 pub enum Event<T: Config> {233 InvulnerableAdded {234 invulnerable: T::AccountId,235 },236 InvulnerableRemoved {237 invulnerable: T::AccountId,238 },239 LicenseObtained {240 account_id: T::AccountId,241 deposit: BalanceOf<T>,242 },243 LicenseReleased {244 account_id: T::AccountId,245 deposit_returned: BalanceOf<T>,246 },247 CandidateAdded {248 account_id: T::AccountId,249 },250 CandidateRemoved {251 account_id: T::AccountId,252 },253 }254255 // Errors inform users that something went wrong.256 #[pallet::error]257 pub enum Error<T> {258 /// Too many candidates259 TooManyCandidates,260 /// Unknown error261 Unknown,262 /// Permission issue263 Permission,264 /// User already holds license to collate265 AlreadyHoldingLicense,266 /// User does not hold a license to collate267 NoLicense,268 /// User is already a candidate269 AlreadyCandidate,270 /// User is not a candidate271 NotCandidate,272 /// Too many invulnerables273 TooManyInvulnerables,274 /// Too few invulnerables275 TooFewInvulnerables,276 /// User is already an Invulnerable277 AlreadyInvulnerable,278 /// User is not an Invulnerable279 NotInvulnerable,280 /// Account has no associated validator ID281 NoAssociatedValidatorId,282 /// Validator ID is not yet registered283 ValidatorNotRegistered,284 }285286 #[pallet::hooks]287 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}288289 #[pallet::call]290 impl<T: Config> Pallet<T> {291 /// Add a collator to the list of invulnerable (fixed) collators.292 #[pallet::call_index(0)]293 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]294 pub fn add_invulnerable(295 origin: OriginFor<T>,296 new: T::AccountId,297 ) -> DispatchResultWithPostInfo {298 T::UpdateOrigin::ensure_origin(origin)?;299300 // check if the new invulnerable has associated validator keys before it is added301 let validator_key = T::ValidatorIdOf::convert(new.clone())302 .ok_or(Error::<T>::NoAssociatedValidatorId)?;303 ensure!(304 T::ValidatorRegistration::is_registered(&validator_key),305 Error::<T>::ValidatorNotRegistered306 );307 if Self::invulnerables().contains(&new) {308 return Ok(().into());309 }310311 <Invulnerables<T>>::try_append(new.clone())312 .map_err(|_| Error::<T>::TooManyInvulnerables)?;313314 // try to offboard the new invulnerable if it was a collator candidate before315 let _ = Self::try_remove_candidate(&new);316317 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });318 Ok(().into())319 }320321 /// Remove a collator from the list of invulnerable (fixed) collators.322 #[pallet::call_index(1)]323 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]324 pub fn remove_invulnerable(325 origin: OriginFor<T>,326 who: T::AccountId,327 ) -> DispatchResultWithPostInfo {328 T::UpdateOrigin::ensure_origin(origin)?;329330 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {331 if invulnerables.len() <= 1 {332 return Err(Error::<T>::TooFewInvulnerables.into());333 }334335 let index = invulnerables336 .into_iter()337 .position(|r| *r == who)338 .ok_or(Error::<T>::NotInvulnerable)?;339 invulnerables.remove(index);340 Ok(())341 })?;342 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });343 Ok(().into())344 }345346 /// Purchase a license on block collation for this account.347 /// It does not make it a collator candidate, use `onboard` afterward. The account must348 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.349 ///350 /// This call is not available to `Invulnerable` collators.351 #[pallet::call_index(2)]352 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]353 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {354 // register_as_candidate355 let who = ensure_signed(origin)?;356357 if LicenseDepositOf::<T>::contains_key(&who) {358 return Err(Error::<T>::AlreadyHoldingLicense.into());359 }360361 let validator_key = T::ValidatorIdOf::convert(who.clone())362 .ok_or(Error::<T>::NoAssociatedValidatorId)?;363 ensure!(364 T::ValidatorRegistration::is_registered(&validator_key),365 Error::<T>::ValidatorNotRegistered366 );367368 let deposit = <LicenseBond<T>>::get();369370 T::Currency::hold(&T::LicenceBondIdentifier::get(), &who, deposit)?;371 LicenseDepositOf::<T>::insert(who.clone(), deposit);372373 Self::deposit_event(Event::LicenseObtained {374 account_id: who,375 deposit,376 });377 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())378 }379380 /// Register this account as a candidate for collators for next sessions.381 /// The account must already hold a license, and cannot offboard immediately during a session.382 ///383 /// This call is not available to `Invulnerable` collators.384 #[pallet::call_index(3)]385 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]386 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {387 // register_as_candidate388 let who = ensure_signed(origin)?;389390 // ensure the user obtained the license.391 ensure!(392 LicenseDepositOf::<T>::contains_key(&who),393 Error::<T>::NoLicense394 );395 // ensure we are below limit.396 let length = <Candidates<T>>::decode_len().unwrap_or_default()397 + <Invulnerables<T>>::decode_len().unwrap_or_default();398 ensure!(399 (length as u32) < <DesiredCollators<T>>::get(),400 Error::<T>::TooManyCandidates401 );402 ensure!(403 !Self::invulnerables().contains(&who),404 Error::<T>::AlreadyInvulnerable405 );406407 let current_count =408 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {409 if candidates.iter().any(|candidate| *candidate == who) {410 Err(Error::<T>::AlreadyCandidate)?411 } else {412 candidates413 .try_push(who.clone())414 .map_err(|_| Error::<T>::TooManyCandidates)?;415 // First authored block is current block plus kick threshold to handle session delay416 <LastAuthoredBlock<T>>::insert(417 who.clone(),418 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),419 );420 Ok(candidates.len())421 }422 })?;423424 Self::deposit_event(Event::CandidateAdded { account_id: who });425 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())426 }427428 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on429 /// session change. The license to `onboard` later at any other time will remain.430 #[pallet::call_index(4)]431 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]432 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {433 // leave_intent434 let who = ensure_signed(origin)?;435 let current_count = Self::try_remove_candidate(&who)?;436437 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())438 }439440 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.441 ///442 /// This call is not available to `Invulnerable` collators.443 #[pallet::call_index(5)]444 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]445 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {446 // leave_intent447 let who = ensure_signed(origin)?;448449 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;450451 Ok(Some(<T as Config>::WeightInfo::release_license(452 current_count as u32,453 ))454 .into())455 }456457 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.458 /// Note that the collator can only leave on session change.459 /// The `LicenseBond` will be unreserved and returned immediately.460 ///461 /// This call is, of course, not applicable to `Invulnerable` collators.462 #[pallet::call_index(6)]463 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]464 pub fn force_release_license(465 origin: OriginFor<T>,466 who: T::AccountId,467 ) -> DispatchResultWithPostInfo {468 // leave_intent469 T::UpdateOrigin::ensure_origin(origin)?;470471 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;472473 Ok(Some(<T as Config>::WeightInfo::force_release_license(474 current_count as u32,475 ))476 .into())477 }478 }479480 impl<T: Config> Pallet<T> {481 /// Get a unique, inaccessible account id from the `PotId`.482 pub fn account_id() -> T::AccountId {483 T::PotId::get().into_account_truncating()484 }485486 /// Removes a candidate and their license, optionally slashed and optionally ignoring,487 /// whether or not they actually are a candidate.488 fn try_remove_candidate_and_release_license(489 who: &T::AccountId,490 should_slash: bool,491 ignore_if_not_candidate: bool,492 ) -> Result<usize, DispatchError> {493 let current_count = Self::try_remove_candidate(who);494 let current_count = if ignore_if_not_candidate495 && current_count == Err(Error::<T>::NotCandidate.into())496 {497 <Candidates<T>>::decode_len().unwrap_or_default()498 } else {499 current_count?500 };501 Self::try_release_license(who, should_slash)?;502 Ok(current_count)503 }504505 /// Removes a candidate from the collator pool for the next session if they exist.506 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {507 let current_count =508 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {509 let index = candidates510 .iter()511 .position(|candidate| *candidate == *who)512 .ok_or(Error::<T>::NotCandidate)?;513 candidates.remove(index);514 <LastAuthoredBlock<T>>::remove(who.clone());515 Ok(candidates.len())516 })?;517 Self::deposit_event(Event::CandidateRemoved {518 account_id: who.clone(),519 });520 Ok(current_count)521 }522523 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.524 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {525 let mut deposit_returned = BalanceOf::<T>::default();526 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {527 if let Some(deposit) = deposit.take() {528 if should_slash {529 let slashed = T::SlashRatio::get() * deposit;530 let remaining = deposit - slashed;531532 let (imbalance, _) =533 T::Currency::slash(&T::LicenceBondIdentifier::get(), who, slashed);534 deposit_returned = remaining;535536 T::Currency::resolve(&T::TreasuryAccountId::get(), imbalance)537 .map_err(|_| DispatchError::Other("Failed to deposit imbalance"))?;538 } else {539 //T::Currency::unreserve(who, deposit);540 deposit_returned = deposit;541 }542543 T::Currency::release(544 &T::LicenceBondIdentifier::get(),545 who,546 deposit_returned,547 Precision::Exact,548 )?;549 Ok(())550 } else {551 Err(Error::<T>::NoLicense.into())552 }553 })?;554 Self::deposit_event(Event::LicenseReleased {555 account_id: who.clone(),556 deposit_returned,557 });558 Ok(())559 }560561 /// Assemble the current set of candidates and invulnerables into the next collator set.562 ///563 /// This is done on the fly, as frequent as we are told to do so, as the session manager.564 pub fn assemble_collators(565 candidates: BoundedVec<T::AccountId, T::MaxCollators>,566 ) -> Vec<T::AccountId> {567 let mut collators = Self::invulnerables().to_vec();568 collators.extend(candidates);569 collators570 }571572 /// Kicks out candidates that did not produce a block in the kick threshold573 /// and **confiscates** their deposits to the treasury.574 pub fn kick_stale_candidates(575 candidates: BoundedVec<T::AccountId, T::MaxCollators>,576 ) -> BoundedVec<T::AccountId, T::MaxCollators> {577 let now = frame_system::Pallet::<T>::block_number();578 let kick_threshold = <KickThreshold<T>>::get();579 candidates580 .into_iter()581 .filter_map(|c| {582 let last_block = <LastAuthoredBlock<T>>::get(c.clone());583 let since_last = now.saturating_sub(last_block);584 if since_last < kick_threshold {585 Some(c)586 } else {587 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);588 if let Err(why) = outcome {589 log::warn!("Failed to kick collator and release license {:?}", why);590 debug_assert!(false, "failed to kick collator and release license {why:?}");591 }592 None593 }594 })595 .collect::<Vec<_>>()596 .try_into()597 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")598 }599 }600601 /// Keep track of number of authored blocks per authority, uncles are counted as well since602 /// they're a valid proof of being online.603 impl<T: Config + pallet_authorship::Config>604 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>605 {606 fn note_author(author: T::AccountId) {607 let pot = Self::account_id();608 // assumes an ED will be sent to pot.609 let reward = T::Currency::balance(&pot)610 .checked_sub(&T::Currency::minimum_balance())611 .unwrap_or_else(Zero::zero)612 .div(2u32.into());613 // `reward` is half of pot account minus ED, this should never fail.614 let _success = T::Currency::transfer(&pot, &author, reward, Preservation::Preserve);615 debug_assert!(_success.is_ok());616 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());617618 frame_system::Pallet::<T>::register_extra_weight_unchecked(619 <T as Config>::WeightInfo::note_author(),620 DispatchClass::Mandatory,621 );622 }623 }624625 /// Play the role of the session manager.626 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {627 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {628 log::info!(629 "assembling new collators for new session {} at #{:?}",630 index,631 <frame_system::Pallet<T>>::block_number(),632 );633634 let candidates = Self::candidates();635 let candidates_len_before = candidates.len();636 let active_candidates = Self::kick_stale_candidates(candidates);637 let removed = candidates_len_before - active_candidates.len();638 let result = Self::assemble_collators(active_candidates);639640 frame_system::Pallet::<T>::register_extra_weight_unchecked(641 <T as Config>::WeightInfo::new_session(642 candidates_len_before as u32,643 removed as u32,644 ),645 DispatchClass::Mandatory,646 );647 Some(result)648 }649 fn start_session(_: SessionIndex) {650 // we don't care.651 }652 fn end_session(_: SessionIndex) {653 // we don't care.654 }655 }656}