git.delta.rocks / unique-network / refs/commits / a64769ff6bff

difftreelog

source

pallets/collator-selection/src/lib.rs21.8 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// 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;9293use frame_support::traits::fungible::Inspect;9495type BalanceOf<T> =96	<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;97#[frame_support::pallet]98pub mod pallet {99	use super::*;100	pub use crate::weights::WeightInfo;101	use core::ops::Div;102	use frame_support::{103		dispatch::{DispatchClass, DispatchResultWithPostInfo},104		inherent::Vec,105		pallet_prelude::*,106		sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},107		traits::{108			EnsureOrigin,109			fungible::{Balanced, BalancedHold, Inspect, InspectHold, Mutate, MutateHold},110			ValidatorRegistration,111			tokens::{Precision, Preservation},112		},113		BoundedVec, PalletId,114	};115	use frame_system::pallet_prelude::*;116	use pallet_session::SessionManager;117	use sp_runtime::{Perbill, traits::Convert};118	use sp_staking::SessionIndex;119120	/// A convertor from collators id. Since this pallet does not have stash/controller, this is121	/// just identity.122	pub struct IdentityCollator;123	impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {124		fn convert(t: T) -> Option<T> {125			Some(t)126		}127	}128129	/// Configure the pallet by specifying the parameters and types on which it depends.130	#[pallet::config]131	pub trait Config: frame_system::Config {132		/// Overarching event type.133		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;134		/// Overarching hold reason.135		type RuntimeHoldReason: From<HoldReason>;136137		type Currency: Mutate<Self::AccountId>138			+ MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>139			+ BalancedHold<Self::AccountId>;140141		/// Origin that can dictate updating parameters of this pallet.142		type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;143144		/// Account Identifier that holds the chain's treasury.145		type TreasuryAccountId: Get<Self::AccountId>;146147		/// Account Identifier from which the internal Pot is generated.148		type PotId: Get<PalletId>;149150		/// Maximum number of candidates and invulnerables that we should have. This is enforced in code.151		type MaxCollators: Get<u32>;152153		/// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.154		type SlashRatio: Get<Perbill>;155156		/// A stable ID for a validator.157		type ValidatorId: Member + Parameter;158159		/// A conversion from account ID to validator ID.160		///161		/// Its cost must be at most one storage read.162		type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;163164		/// Validate a user is registered165		type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;166167		/// The weight information of this pallet.168		type WeightInfo: WeightInfo;169170		type DesiredCollators: Get<u32>;171172		type LicenseBond: Get<BalanceOf<Self>>;173174		type KickThreshold: Get<BlockNumberFor<Self>>;175	}176177	#[pallet::composite_enum]178	pub enum HoldReason {179		/// The funds are held as the license bond.180		LicenseBond,181	}182183	#[pallet::pallet]184	pub struct Pallet<T>(_);185186	/// The invulnerable, fixed collators.187	#[pallet::storage]188	#[pallet::getter(fn invulnerables)]189	pub type Invulnerables<T: Config> =190		StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;191192	/// The (community) collation license holders.193	#[pallet::storage]194	#[pallet::getter(fn license_deposit_of)]195	pub type LicenseDepositOf<T: Config> =196		StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;197198	/// The (community, limited) collation candidates.199	#[pallet::storage]200	#[pallet::getter(fn candidates)]201	pub type Candidates<T: Config> =202		StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;203204	/// Last block authored by collator.205	#[pallet::storage]206	#[pallet::getter(fn last_authored_block)]207	pub type LastAuthoredBlock<T: Config> =208		StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor<T>, ValueQuery>;209210	#[pallet::genesis_config]211	pub struct GenesisConfig<T: Config> {212		pub invulnerables: Vec<T::AccountId>,213	}214215	impl<T: Config> Default for GenesisConfig<T> {216		fn default() -> Self {217			Self {218				invulnerables: Default::default(),219			}220		}221	}222223	#[pallet::genesis_build]224	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {225		fn build(&self) {226			use sp_std::collections::btree_set::BTreeSet;227228			let duplicate_invulnerables = self.invulnerables.iter().collect::<BTreeSet<_>>();229			assert!(230				duplicate_invulnerables.len() == self.invulnerables.len(),231				"duplicate invulnerables in genesis."232			);233234			let bounded_invulnerables =235				BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())236					.expect("genesis invulnerables are more than T::MaxCollators");237238			<Invulnerables<T>>::put(bounded_invulnerables);239		}240	}241242	#[pallet::event]243	#[pallet::generate_deposit(pub(super) fn deposit_event)]244	pub enum Event<T: Config> {245		InvulnerableAdded {246			invulnerable: T::AccountId,247		},248		InvulnerableRemoved {249			invulnerable: T::AccountId,250		},251		LicenseObtained {252			account_id: T::AccountId,253			deposit: BalanceOf<T>,254		},255		LicenseReleased {256			account_id: T::AccountId,257			deposit_returned: BalanceOf<T>,258		},259		CandidateAdded {260			account_id: T::AccountId,261		},262		CandidateRemoved {263			account_id: T::AccountId,264		},265	}266267	// Errors inform users that something went wrong.268	#[pallet::error]269	pub enum Error<T> {270		/// Too many candidates271		TooManyCandidates,272		/// Unknown error273		Unknown,274		/// Permission issue275		Permission,276		/// User already holds license to collate277		AlreadyHoldingLicense,278		/// User does not hold a license to collate279		NoLicense,280		/// User is already a candidate281		AlreadyCandidate,282		/// User is not a candidate283		NotCandidate,284		/// Too many invulnerables285		TooManyInvulnerables,286		/// Too few invulnerables287		TooFewInvulnerables,288		/// User is already an Invulnerable289		AlreadyInvulnerable,290		/// User is not an Invulnerable291		NotInvulnerable,292		/// Account has no associated validator ID293		NoAssociatedValidatorId,294		/// Validator ID is not yet registered295		ValidatorNotRegistered,296	}297298	#[pallet::hooks]299	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}300301	#[pallet::call]302	impl<T: Config> Pallet<T> {303		/// Add a collator to the list of invulnerable (fixed) collators.304		#[pallet::call_index(0)]305		#[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]306		pub fn add_invulnerable(307			origin: OriginFor<T>,308			new: T::AccountId,309		) -> DispatchResultWithPostInfo {310			T::UpdateOrigin::ensure_origin(origin)?;311312			// check if the new invulnerable has associated validator keys before it is added313			let validator_key = T::ValidatorIdOf::convert(new.clone())314				.ok_or(Error::<T>::NoAssociatedValidatorId)?;315			ensure!(316				T::ValidatorRegistration::is_registered(&validator_key),317				Error::<T>::ValidatorNotRegistered318			);319			if Self::invulnerables().contains(&new) {320				return Ok(().into());321			}322323			<Invulnerables<T>>::try_append(new.clone())324				.map_err(|_| Error::<T>::TooManyInvulnerables)?;325326			// try to offboard the new invulnerable if it was a collator candidate before327			let _ = Self::try_remove_candidate(&new);328329			Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });330			Ok(().into())331		}332333		/// Remove a collator from the list of invulnerable (fixed) collators.334		#[pallet::call_index(1)]335		#[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]336		pub fn remove_invulnerable(337			origin: OriginFor<T>,338			who: T::AccountId,339		) -> DispatchResultWithPostInfo {340			T::UpdateOrigin::ensure_origin(origin)?;341342			<Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {343				if invulnerables.len() <= 1 {344					return Err(Error::<T>::TooFewInvulnerables.into());345				}346347				let index = invulnerables348					.into_iter()349					.position(|r| *r == who)350					.ok_or(Error::<T>::NotInvulnerable)?;351				invulnerables.remove(index);352				Ok(())353			})?;354			Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });355			Ok(().into())356		}357358		/// Purchase a license on block collation for this account.359		/// It does not make it a collator candidate, use `onboard` afterward. The account must360		/// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.361		///362		/// This call is not available to `Invulnerable` collators.363		#[pallet::call_index(2)]364		#[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]365		pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {366			// register_as_candidate367			let who = ensure_signed(origin)?;368369			if LicenseDepositOf::<T>::contains_key(&who) {370				return Err(Error::<T>::AlreadyHoldingLicense.into());371			}372373			let validator_key = T::ValidatorIdOf::convert(who.clone())374				.ok_or(Error::<T>::NoAssociatedValidatorId)?;375			ensure!(376				T::ValidatorRegistration::is_registered(&validator_key),377				Error::<T>::ValidatorNotRegistered378			);379380			let deposit = T::LicenseBond::get();381382			T::Currency::hold(&HoldReason::LicenseBond.into(), &who, deposit)?;383			LicenseDepositOf::<T>::insert(who.clone(), deposit);384385			Self::deposit_event(Event::LicenseObtained {386				account_id: who,387				deposit,388			});389			Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())390		}391392		/// Register this account as a candidate for collators for next sessions.393		/// The account must already hold a license, and cannot offboard immediately during a session.394		///395		/// This call is not available to `Invulnerable` collators.396		#[pallet::call_index(3)]397		#[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]398		pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {399			// register_as_candidate400			let who = ensure_signed(origin)?;401402			// ensure the user obtained the license.403			ensure!(404				LicenseDepositOf::<T>::contains_key(&who),405				Error::<T>::NoLicense406			);407			// ensure we are below limit.408			let length = <Candidates<T>>::decode_len().unwrap_or_default()409				+ <Invulnerables<T>>::decode_len().unwrap_or_default();410			ensure!(411				(length as u32) < T::DesiredCollators::get(),412				Error::<T>::TooManyCandidates413			);414			ensure!(415				!Self::invulnerables().contains(&who),416				Error::<T>::AlreadyInvulnerable417			);418419			let current_count =420				<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {421					if candidates.iter().any(|candidate| *candidate == who) {422						Err(Error::<T>::AlreadyCandidate)?423					} else {424						candidates425							.try_push(who.clone())426							.map_err(|_| Error::<T>::TooManyCandidates)?;427						// First authored block is current block plus kick threshold to handle session delay428						<LastAuthoredBlock<T>>::insert(429							who.clone(),430							frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),431						);432						Ok(candidates.len())433					}434				})?;435436			Self::deposit_event(Event::CandidateAdded { account_id: who });437			Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())438		}439440		/// Deregister `origin` as a collator candidate. Note that the collator can only leave on441		/// session change. The license to `onboard` later at any other time will remain.442		#[pallet::call_index(4)]443		#[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]444		pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {445			// leave_intent446			let who = ensure_signed(origin)?;447			let current_count = Self::try_remove_candidate(&who)?;448449			Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())450		}451452		/// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.453		///454		/// This call is not available to `Invulnerable` collators.455		#[pallet::call_index(5)]456		#[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]457		pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {458			// leave_intent459			let who = ensure_signed(origin)?;460461			let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;462463			Ok(Some(<T as Config>::WeightInfo::release_license(464				current_count as u32,465			))466			.into())467		}468469		/// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.470		/// Note that the collator can only leave on session change.471		/// The `LicenseBond` will be unreserved and returned immediately.472		///473		/// This call is, of course, not applicable to `Invulnerable` collators.474		#[pallet::call_index(6)]475		#[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]476		pub fn force_release_license(477			origin: OriginFor<T>,478			who: T::AccountId,479		) -> DispatchResultWithPostInfo {480			// leave_intent481			T::UpdateOrigin::ensure_origin(origin)?;482483			let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;484485			Ok(Some(<T as Config>::WeightInfo::force_release_license(486				current_count as u32,487			))488			.into())489		}490	}491492	impl<T: Config> Pallet<T> {493		/// Get a unique, inaccessible account id from the `PotId`.494		pub fn account_id() -> T::AccountId {495			T::PotId::get().into_account_truncating()496		}497498		/// Removes a candidate and their license, optionally slashed and optionally ignoring,499		/// whether or not they actually are a candidate.500		fn try_remove_candidate_and_release_license(501			who: &T::AccountId,502			should_slash: bool,503			ignore_if_not_candidate: bool,504		) -> Result<usize, DispatchError> {505			let current_count = Self::try_remove_candidate(who);506			let current_count = if ignore_if_not_candidate507				&& current_count == Err(Error::<T>::NotCandidate.into())508			{509				<Candidates<T>>::decode_len().unwrap_or_default()510			} else {511				current_count?512			};513			Self::try_release_license(who, should_slash)?;514			Ok(current_count)515		}516517		/// Removes a candidate from the collator pool for the next session if they exist.518		fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {519			let current_count =520				<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {521					let index = candidates522						.iter()523						.position(|candidate| *candidate == *who)524						.ok_or(Error::<T>::NotCandidate)?;525					candidates.remove(index);526					<LastAuthoredBlock<T>>::remove(who.clone());527					Ok(candidates.len())528				})?;529			Self::deposit_event(Event::CandidateRemoved {530				account_id: who.clone(),531			});532			Ok(current_count)533		}534535		/// Removes a candidate if they exist and sends them back their deposit, optionally slashed.536		fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {537			let mut deposit_returned = BalanceOf::<T>::default();538			LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {539				if let Some(deposit) = deposit.take() {540					if should_slash {541						let slashed = T::SlashRatio::get() * deposit;542						let remaining = deposit - slashed;543544						let (imbalance, _) =545							T::Currency::slash(&HoldReason::LicenseBond.into(), who, slashed);546						deposit_returned = remaining;547548						T::Currency::resolve(&T::TreasuryAccountId::get(), imbalance)549							.map_err(|_| DispatchError::Other("Failed to deposit imbalance"))?;550					} else {551						deposit_returned = deposit;552					}553554					T::Currency::release(555						&HoldReason::LicenseBond.into(),556						who,557						deposit_returned,558						Precision::Exact,559					)?;560					Ok(())561				} else {562					Err(Error::<T>::NoLicense.into())563				}564			})?;565			Self::deposit_event(Event::LicenseReleased {566				account_id: who.clone(),567				deposit_returned,568			});569			Ok(())570		}571572		/// Assemble the current set of candidates and invulnerables into the next collator set.573		///574		/// This is done on the fly, as frequent as we are told to do so, as the session manager.575		pub fn assemble_collators(576			candidates: BoundedVec<T::AccountId, T::MaxCollators>,577		) -> Vec<T::AccountId> {578			let mut collators = Self::invulnerables().to_vec();579			collators.extend(candidates);580			collators581		}582583		/// Kicks out candidates that did not produce a block in the kick threshold584		/// and **confiscates** their deposits to the treasury.585		pub fn kick_stale_candidates(586			candidates: BoundedVec<T::AccountId, T::MaxCollators>,587		) -> BoundedVec<T::AccountId, T::MaxCollators> {588			let now = frame_system::Pallet::<T>::block_number();589			let kick_threshold = T::KickThreshold::get();590			candidates591				.into_iter()592				.filter_map(|c| {593					let last_block = <LastAuthoredBlock<T>>::get(c.clone());594					let since_last = now.saturating_sub(last_block);595					if since_last < kick_threshold {596						Some(c)597					} else {598						let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);599						if let Err(why) = outcome {600							log::warn!("Failed to kick collator and release license {:?}", why);601							debug_assert!(false, "failed to kick collator and release license {why:?}");602						}603						None604					}605				})606				.collect::<Vec<_>>()607				.try_into()608				.expect("filter_map operation can't result in a bounded vec larger than its original; qed")609		}610	}611612	/// Keep track of number of authored blocks per authority, uncles are counted as well since613	/// they're a valid proof of being online.614	impl<T: Config + pallet_authorship::Config>615		pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T>616	{617		fn note_author(author: T::AccountId) {618			let pot = Self::account_id();619			// assumes an ED will be sent to pot.620			let reward = T::Currency::balance(&pot)621				.checked_sub(&T::Currency::minimum_balance())622				.unwrap_or_else(Zero::zero)623				.div(2u32.into());624625			if !reward.is_zero() {626				// `reward` is half of pot account minus ED, this should never fail.627				let _success = T::Currency::transfer(&pot, &author, reward, Preservation::Preserve);628				debug_assert!(_success.is_ok());629			}630			<LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());631632			frame_system::Pallet::<T>::register_extra_weight_unchecked(633				<T as Config>::WeightInfo::note_author(),634				DispatchClass::Mandatory,635			);636		}637	}638639	/// Play the role of the session manager.640	impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {641		fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {642			log::info!(643				"assembling new collators for new session {} at #{:?}",644				index,645				<frame_system::Pallet<T>>::block_number(),646			);647648			let candidates = Self::candidates();649			let candidates_len_before = candidates.len();650			let active_candidates = Self::kick_stale_candidates(candidates);651			let removed = candidates_len_before - active_candidates.len();652			let result = Self::assemble_collators(active_candidates);653654			frame_system::Pallet::<T>::register_extra_weight_unchecked(655				<T as Config>::WeightInfo::new_session(656					candidates_len_before as u32,657					removed as u32,658				),659				DispatchClass::Mandatory,660			);661			Some(result)662		}663		fn start_session(_: SessionIndex) {664			// we don't care.665		}666		fn end_session(_: SessionIndex) {667			// we don't care.668		}669	}670}