12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879#![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 122 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 131 #[pallet::config]132 pub trait Config: frame_system::Config + pallet_configuration::Config {133 134 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;135136 137 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;138139 140 type TreasuryAccountId: Get<Self::AccountId>;141142 143 type PotId: Get<PalletId>;144145 146 type MaxCollators: Get<u32>;147148 149 type SlashRatio: Get<Perbill>;150151 152 type ValidatorId: Member + Parameter;153154 155 156 157 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;158159 160 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;161162 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 173 #[pallet::storage]174 #[pallet::getter(fn invulnerables)]175 pub type Invulnerables<T: Config> =176 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;177178 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 185 #[pallet::storage]186 #[pallet::getter(fn candidates)]187 pub type Candidates<T: Config> =188 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;189190 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 256 #[pallet::error]257 pub enum Error<T> {258 259 TooManyCandidates,260 261 Unknown,262 263 Permission,264 265 AlreadyHoldingLicense,266 267 NoLicense,268 269 AlreadyCandidate,270 271 NotCandidate,272 273 TooManyInvulnerables,274 275 TooFewInvulnerables,276 277 AlreadyInvulnerable,278 279 NotInvulnerable,280 281 NoAssociatedValidatorId,282 283 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 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 301 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 315 let _ = Self::try_remove_candidate(&new);316317 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });318 Ok(().into())319 }320321 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 347 348 349 350 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 355 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()) 378 }379380 381 382 383 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 388 let who = ensure_signed(origin)?;389390 391 ensure!(392 LicenseDepositOf::<T>::contains_key(&who),393 Error::<T>::NoLicense394 );395 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 416 <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 429 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 434 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 441 442 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 447 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 458 459 460 461 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 469 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 482 pub fn account_id() -> T::AccountId {483 T::PotId::get().into_account_truncating()484 }485486 487 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 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 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 deposit_returned = deposit;540 }541542 T::Currency::release(543 &T::LicenceBondIdentifier::get(),544 who,545 deposit_returned,546 Precision::Exact,547 )?;548 Ok(())549 } else {550 Err(Error::<T>::NoLicense.into())551 }552 })?;553 Self::deposit_event(Event::LicenseReleased {554 account_id: who.clone(),555 deposit_returned,556 });557 Ok(())558 }559560 561 562 563 pub fn assemble_collators(564 candidates: BoundedVec<T::AccountId, T::MaxCollators>,565 ) -> Vec<T::AccountId> {566 let mut collators = Self::invulnerables().to_vec();567 collators.extend(candidates);568 collators569 }570571 572 573 pub fn kick_stale_candidates(574 candidates: BoundedVec<T::AccountId, T::MaxCollators>,575 ) -> BoundedVec<T::AccountId, T::MaxCollators> {576 let now = frame_system::Pallet::<T>::block_number();577 let kick_threshold = <KickThreshold<T>>::get();578 candidates579 .into_iter()580 .filter_map(|c| {581 let last_block = <LastAuthoredBlock<T>>::get(c.clone());582 let since_last = now.saturating_sub(last_block);583 if since_last < kick_threshold {584 Some(c)585 } else {586 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);587 if let Err(why) = outcome {588 log::warn!("Failed to kick collator and release license {:?}", why);589 debug_assert!(false, "failed to kick collator and release license {why:?}");590 }591 None592 }593 })594 .collect::<Vec<_>>()595 .try_into()596 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")597 }598 }599600 601 602 impl<T: Config + pallet_authorship::Config>603 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>604 {605 fn note_author(author: T::AccountId) {606 let pot = Self::account_id();607 608 let reward = T::Currency::balance(&pot)609 .checked_sub(&T::Currency::minimum_balance())610 .unwrap_or_else(Zero::zero)611 .div(2u32.into());612613 if !reward.is_zero() {614 615 let _success = T::Currency::transfer(&pot, &author, reward, Preservation::Preserve);616 debug_assert!(_success.is_ok());617 }618 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());619620 frame_system::Pallet::<T>::register_extra_weight_unchecked(621 <T as Config>::WeightInfo::note_author(),622 DispatchClass::Mandatory,623 );624 }625 }626627 628 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {629 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {630 log::info!(631 "assembling new collators for new session {} at #{:?}",632 index,633 <frame_system::Pallet<T>>::block_number(),634 );635636 let candidates = Self::candidates();637 let candidates_len_before = candidates.len();638 let active_candidates = Self::kick_stale_candidates(candidates);639 let removed = candidates_len_before - active_candidates.len();640 let result = Self::assemble_collators(active_candidates);641642 frame_system::Pallet::<T>::register_extra_weight_unchecked(643 <T as Config>::WeightInfo::new_session(644 candidates_len_before as u32,645 removed as u32,646 ),647 DispatchClass::Mandatory,648 );649 Some(result)650 }651 fn start_session(_: SessionIndex) {652 653 }654 fn end_session(_: SessionIndex) {655 656 }657 }658}