git.delta.rocks / unique-network / refs/commits / 8f7419bc1620

difftreelog

source

pallets/collator-selection/src/lib.rs18.7 KiBsourcehistory
1// 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//! Collator Selection pallet.34//!35//! A pallet to manage collators in a parachain.36//!37//! ## Overview38//!39//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a40//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT41//! safety assumptions of the chosen set.42//!43//! ## Terminology44//!45//! - Collator: A parachain block producer.46//! - Bond: An amount of `Balance` _reserved_ for candidate registration.47//! - Invulnerable: An account guaranteed to be in the collator set.48//!49//! ## Implementation50//!51//! The final `Collators` are aggregated from two individual lists:52//!53//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be54//!    collators.55//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as56//!    a final collator.57//!58//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve59//! manner.60//!61//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates62//! fall below MinCandidates. This is for potential disaster recovery scenarios.63//!64//! ### Rewards65//!66//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the67//! collator who authored it receives:68//!69//! - Half the value of the Pot.70//! - Half the value of the transaction fees within the block. The other half of the transaction71//!   fees are deposited into the Pot.72//!73//! To initiate rewards an ED needs to be transferred to the pot address.74//!75//! Note: Eventually the Pot distribution may be modified as discussed in76//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7778#![cfg_attr(not(feature = "std"), no_std)]7980pub use pallet::*;8182#[cfg(test)]83mod mock;8485#[cfg(test)]86mod tests;8788#[cfg(feature = "runtime-benchmarks")]89mod benchmarking;90pub mod weights;9192#[frame_support::pallet]93pub mod pallet {94	pub use crate::weights::WeightInfo;95	use core::ops::Div;96	use frame_support::{97		dispatch::{DispatchClass, DispatchResultWithPostInfo},98		inherent::Vec,99		pallet_prelude::*,100		sp_runtime::{101			traits::{AccountIdConversion, CheckedSub, Saturating, Zero},102			RuntimeDebug,103		},104		traits::{105			Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,106			ValidatorRegistration,107		},108		BoundedVec, PalletId,109	};110	use frame_system::{pallet_prelude::*, Config as SystemConfig};111	use pallet_session::SessionManager;112	use sp_runtime::traits::Convert;113	use sp_staking::SessionIndex;114115	type BalanceOf<T> =116		<<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;117118	/// A convertor from collators id. Since this pallet does not have stash/controller, this is119	/// just identity.120	pub struct IdentityCollator;121	impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {122		fn convert(t: T) -> Option<T> {123			Some(t)124		}125	}126127	/// Configure the pallet by specifying the parameters and types on which it depends.128	#[pallet::config]129	pub trait Config: frame_system::Config {130		/// Overarching event type.131		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;132133		/// The currency mechanism.134		type Currency: ReservableCurrency<Self::AccountId>;135136		/// Origin that can dictate updating parameters of this pallet.137		type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;138139		/// Account Identifier from which the internal Pot is generated.140		type PotId: Get<PalletId>;141142		/// Maximum number of candidates that we should have. This is enforced in code.143		///144		/// This does not take into account the invulnerables.145		type MaxCandidates: Get<u32>;146147		/// Minimum number of candidates that we should have. This is used for disaster recovery.148		///149		/// This does not take into account the invulnerables.150		type MinCandidates: Get<u32>;151152		/// Maximum number of invulnerables. This is enforced in code.153		type MaxInvulnerables: Get<u32>;154155		// Will be kicked if block is not produced in threshold.156		type KickThreshold: Get<Self::BlockNumber>;157158		/// A stable ID for a validator.159		type ValidatorId: Member + Parameter;160161		/// A conversion from account ID to validator ID.162		///163		/// Its cost must be at most one storage read.164		type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;165166		/// Validate a user is registered167		type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;168169		/// The weight information of this pallet.170		type WeightInfo: WeightInfo;171	}172173	/// Basic information about a collation candidate.174	#[derive(175		PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,176	)]177	pub struct CandidateInfo<AccountId, Balance> {178		/// Account identifier.179		pub who: AccountId,180		/// Reserved deposit.181		pub deposit: Balance,182	}183184	#[pallet::pallet]185	#[pallet::generate_store(pub(super) trait Store)]186	pub struct Pallet<T>(_);187188	/// The invulnerable, fixed collators.189	#[pallet::storage]190	#[pallet::getter(fn invulnerables)]191	pub type Invulnerables<T: Config> =192		StorageValue<_, BoundedVec<T::AccountId, T::MaxInvulnerables>, ValueQuery>;193194	/// The (community, limited) collation candidates.195	#[pallet::storage]196	#[pallet::getter(fn candidates)]197	pub type Candidates<T: Config> = StorageValue<198		_,199		BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,200		ValueQuery,201	>;202203	/// Last block authored by collator.204	#[pallet::storage]205	#[pallet::getter(fn last_authored_block)]206	pub type LastAuthoredBlock<T: Config> =207		StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;208209	/// Desired number of candidates.210	///211	/// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct.212	#[pallet::storage]213	#[pallet::getter(fn desired_candidates)]214	pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;215216	/// Fixed amount to deposit to become a collator.217	///218	/// When a collator calls `leave_intent` they immediately receive the deposit back.219	#[pallet::storage]220	#[pallet::getter(fn candidacy_bond)]221	pub type CandidacyBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;222223	#[pallet::genesis_config]224	pub struct GenesisConfig<T: Config> {225		pub invulnerables: Vec<T::AccountId>,226		pub candidacy_bond: BalanceOf<T>,227		pub desired_candidates: u32,228	}229230	#[cfg(feature = "std")]231	impl<T: Config> Default for GenesisConfig<T> {232		fn default() -> Self {233			Self {234				invulnerables: Default::default(),235				candidacy_bond: Default::default(),236				desired_candidates: Default::default(),237			}238		}239	}240241	#[pallet::genesis_build]242	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {243		fn build(&self) {244			let duplicate_invulnerables =245				self.invulnerables.iter().collect::<std::collections::BTreeSet<_>>();246			assert!(247				duplicate_invulnerables.len() == self.invulnerables.len(),248				"duplicate invulnerables in genesis."249			);250251			let bounded_invulnerables =252				BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())253					.expect("genesis invulnerables are more than T::MaxInvulnerables");254			assert!(255				T::MaxCandidates::get() >= self.desired_candidates,256				"genesis desired_candidates are more than T::MaxCandidates",257			);258259			<DesiredCandidates<T>>::put(&self.desired_candidates);260			<CandidacyBond<T>>::put(&self.candidacy_bond);261			<Invulnerables<T>>::put(bounded_invulnerables);262		}263	}264265	#[pallet::event]266	#[pallet::generate_deposit(pub(super) fn deposit_event)]267	pub enum Event<T: Config> {268		NewInvulnerables { invulnerables: Vec<T::AccountId> },269		NewDesiredCandidates { desired_candidates: u32 },270		NewCandidacyBond { bond_amount: BalanceOf<T> },271		CandidateAdded { account_id: T::AccountId, deposit: BalanceOf<T> },272		CandidateRemoved { account_id: T::AccountId },273	}274275	// Errors inform users that something went wrong.276	#[pallet::error]277	pub enum Error<T> {278		/// Too many candidates279		TooManyCandidates,280		/// Too few candidates281		TooFewCandidates,282		/// Unknown error283		Unknown,284		/// Permission issue285		Permission,286		/// User is already a candidate287		AlreadyCandidate,288		/// User is not a candidate289		NotCandidate,290		/// Too many invulnerables291		TooManyInvulnerables,292		/// User is already an Invulnerable293		AlreadyInvulnerable,294		/// Account has no associated validator ID295		NoAssociatedValidatorId,296		/// Validator ID is not yet registered297		ValidatorNotRegistered,298	}299300	#[pallet::hooks]301	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}302303	#[pallet::call]304	impl<T: Config> Pallet<T> {305		/// Set the list of invulnerable (fixed) collators.306		#[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]307		pub fn set_invulnerables(308			origin: OriginFor<T>,309			new: Vec<T::AccountId>,310		) -> DispatchResultWithPostInfo {311			T::UpdateOrigin::ensure_origin(origin)?;312			let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)313				.map_err(|_| Error::<T>::TooManyInvulnerables)?;314315			// check if the invulnerables have associated validator keys before they are set316			for account_id in bounded_invulnerables.iter() {317				let validator_key = T::ValidatorIdOf::convert(account_id.clone())318					.ok_or(Error::<T>::NoAssociatedValidatorId)?;319				ensure!(320					T::ValidatorRegistration::is_registered(&validator_key),321					Error::<T>::ValidatorNotRegistered322				);323			}324325			<Invulnerables<T>>::put(&bounded_invulnerables);326			Self::deposit_event(Event::NewInvulnerables {327				invulnerables: bounded_invulnerables.to_vec(),328			});329			Ok(().into())330		}331332		/// Set the ideal number of collators (not including the invulnerables).333		/// If lowering this number, then the number of running collators could be higher than this figure.334		/// Aside from that edge case, there should be no other way to have more collators than the desired number.335		#[pallet::weight(T::WeightInfo::set_desired_candidates())]336		pub fn set_desired_candidates(337			origin: OriginFor<T>,338			max: u32,339		) -> DispatchResultWithPostInfo {340			T::UpdateOrigin::ensure_origin(origin)?;341			// we trust origin calls, this is just a for more accurate benchmarking342			if max > T::MaxCandidates::get() {343				log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");344			}345			<DesiredCandidates<T>>::put(&max);346			Self::deposit_event(Event::NewDesiredCandidates { desired_candidates: max });347			Ok(().into())348		}349350		/// Set the candidacy bond amount.351		#[pallet::weight(T::WeightInfo::set_candidacy_bond())]352		pub fn set_candidacy_bond(353			origin: OriginFor<T>,354			bond: BalanceOf<T>,355		) -> DispatchResultWithPostInfo {356			T::UpdateOrigin::ensure_origin(origin)?;357			<CandidacyBond<T>>::put(&bond);358			Self::deposit_event(Event::NewCandidacyBond { bond_amount: bond });359			Ok(().into())360		}361362		/// Register this account as a collator candidate. The account must (a) already have363		/// registered session keys and (b) be able to reserve the `CandidacyBond`.364		///365		/// This call is not available to `Invulnerable` collators.366		#[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))]367		pub fn register_as_candidate(origin: OriginFor<T>) -> DispatchResultWithPostInfo {368			let who = ensure_signed(origin)?;369370			// ensure we are below limit.371			let length = <Candidates<T>>::decode_len().unwrap_or_default();372			ensure!((length as u32) < Self::desired_candidates(), Error::<T>::TooManyCandidates);373			ensure!(!Self::invulnerables().contains(&who), Error::<T>::AlreadyInvulnerable);374375			let validator_key = T::ValidatorIdOf::convert(who.clone())376				.ok_or(Error::<T>::NoAssociatedValidatorId)?;377			ensure!(378				T::ValidatorRegistration::is_registered(&validator_key),379				Error::<T>::ValidatorNotRegistered380			);381382			let deposit = Self::candidacy_bond();383			// First authored block is current block plus kick threshold to handle session delay384			let incoming = CandidateInfo { who: who.clone(), deposit };385386			let current_count =387				<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {388					if candidates.iter().any(|candidate| candidate.who == who) {389						Err(Error::<T>::AlreadyCandidate)?390					} else {391						T::Currency::reserve(&who, deposit)?;392						candidates.try_push(incoming).map_err(|_| Error::<T>::TooManyCandidates)?;393						<LastAuthoredBlock<T>>::insert(394							who.clone(),395							frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),396						);397						Ok(candidates.len())398					}399				})?;400401			Self::deposit_event(Event::CandidateAdded { account_id: who, deposit });402			Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())403		}404405		/// Deregister `origin` as a collator candidate. Note that the collator can only leave on406		/// session change. The `CandidacyBond` will be unreserved immediately.407		///408		/// This call will fail if the total number of candidates would drop below `MinCandidates`.409		///410		/// This call is not available to `Invulnerable` collators.411		#[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]412		pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {413			let who = ensure_signed(origin)?;414			ensure!(415				Self::candidates().len() as u32 > T::MinCandidates::get(),416				Error::<T>::TooFewCandidates417			);418			let current_count = Self::try_remove_candidate(&who)?;419420			Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())421		}422	}423424	impl<T: Config> Pallet<T> {425		/// Get a unique, inaccessible account id from the `PotId`.426		pub fn account_id() -> T::AccountId {427			T::PotId::get().into_account_truncating()428		}429430		/// Removes a candidate if they exist and sends them back their deposit431		fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {432			let current_count =433				<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {434					let index = candidates435						.iter()436						.position(|candidate| candidate.who == *who)437						.ok_or(Error::<T>::NotCandidate)?;438					let candidate = candidates.remove(index);439					T::Currency::unreserve(who, candidate.deposit);440					<LastAuthoredBlock<T>>::remove(who.clone());441					Ok(candidates.len())442				})?;443			Self::deposit_event(Event::CandidateRemoved { account_id: who.clone() });444			Ok(current_count)445		}446447		/// Assemble the current set of candidates and invulnerables into the next collator set.448		///449		/// This is done on the fly, as frequent as we are told to do so, as the session manager.450		pub fn assemble_collators(451			candidates: BoundedVec<T::AccountId, T::MaxCandidates>,452		) -> Vec<T::AccountId> {453			let mut collators = Self::invulnerables().to_vec();454			collators.extend(candidates);455			collators456		}457458		/// Kicks out candidates that did not produce a block in the kick threshold459		/// and refund their deposits.460		pub fn kick_stale_candidates(461			candidates: BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,462		) -> BoundedVec<T::AccountId, T::MaxCandidates> {463			let now = frame_system::Pallet::<T>::block_number();464			let kick_threshold = T::KickThreshold::get();465			candidates466				.into_iter()467				.filter_map(|c| {468					let last_block = <LastAuthoredBlock<T>>::get(c.who.clone());469					let since_last = now.saturating_sub(last_block);470					if since_last < kick_threshold ||471						Self::candidates().len() as u32 <= T::MinCandidates::get()472					{473						Some(c.who)474					} else {475						let outcome = Self::try_remove_candidate(&c.who);476						if let Err(why) = outcome {477							log::warn!("Failed to remove candidate {:?}", why);478							debug_assert!(false, "failed to remove candidate {:?}", why);479						}480						None481					}482				})483				.collect::<Vec<_>>()484				.try_into()485				.expect("filter_map operation can't result in a bounded vec larger than its original; qed")486		}487	}488489	/// Keep track of number of authored blocks per authority, uncles are counted as well since490	/// they're a valid proof of being online.491	impl<T: Config + pallet_authorship::Config>492		pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>493	{494		fn note_author(author: T::AccountId) {495			let pot = Self::account_id();496			// assumes an ED will be sent to pot.497			let reward = T::Currency::free_balance(&pot)498				.checked_sub(&T::Currency::minimum_balance())499				.unwrap_or_else(Zero::zero)500				.div(2u32.into());501			// `reward` is half of pot account minus ED, this should never fail.502			let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);503			debug_assert!(_success.is_ok());504			<LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());505506			frame_system::Pallet::<T>::register_extra_weight_unchecked(507				T::WeightInfo::note_author(),508				DispatchClass::Mandatory,509			);510		}511512		fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {513			//TODO can we ignore this?514		}515	}516517	/// Play the role of the session manager.518	impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {519		fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {520			log::info!(521				"assembling new collators for new session {} at #{:?}",522				index,523				<frame_system::Pallet<T>>::block_number(),524			);525526			let candidates = Self::candidates();527			let candidates_len_before = candidates.len();528			let active_candidates = Self::kick_stale_candidates(candidates);529			let removed = candidates_len_before - active_candidates.len();530			let result = Self::assemble_collators(active_candidates);531532			frame_system::Pallet::<T>::register_extra_weight_unchecked(533				T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),534				DispatchClass::Mandatory,535			);536			Some(result)537		}538		fn start_session(_: SessionIndex) {539			// we don't care.540		}541		fn end_session(_: SessionIndex) {542			// we don't care.543		}544	}545}