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 pub use crate::weights::WeightInfo;96 use core::ops::Div;97 use frame_support::{98 dispatch::{DispatchClass, DispatchResultWithPostInfo},99 inherent::Vec,100 pallet_prelude::*,101 sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},102 traits::{103 Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,104 ValidatorRegistration,105 },106 BoundedVec, PalletId,107 };108 use frame_system::pallet_prelude::*;109 use pallet_session::SessionManager;110 use sp_runtime::{Perbill, traits::Convert};111 use pallet_configuration::{112 CollatorSelectionDesiredCollatorsOverride as DesiredCollators,113 CollatorSelectionLicenseBondOverride as LicenseBond,114 CollatorSelectionKickThresholdOverride as KickThreshold, BalanceOf,115 };116 use sp_staking::SessionIndex;117118 119 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 128 #[pallet::config]129 pub trait Config: frame_system::Config + pallet_configuration::Config {130 131 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;132133 134 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;135136 137 type TreasuryAccountId: Get<Self::AccountId>;138139 140 type PotId: Get<PalletId>;141142 143 type MaxCollators: Get<u32>;144145 146 type SlashRatio: Get<Perbill>;147148 149 type ValidatorId: Member + Parameter;150151 152 153 154 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;155156 157 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;158159 160 type WeightInfo: WeightInfo;161 }162163 #[pallet::pallet]164 #[pallet::generate_store(pub(super) trait Store)]165 pub struct Pallet<T>(_);166167 168 #[pallet::storage]169 #[pallet::getter(fn invulnerables)]170 pub type Invulnerables<T: Config> =171 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;172173 174 #[pallet::storage]175 #[pallet::getter(fn license_deposit_of)]176 pub type LicenseDepositOf<T: Config> =177 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;178179 180 #[pallet::storage]181 #[pallet::getter(fn candidates)]182 pub type Candidates<T: Config> =183 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;184185 186 #[pallet::storage]187 #[pallet::getter(fn last_authored_block)]188 pub type LastAuthoredBlock<T: Config> =189 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;190191 #[pallet::genesis_config]192 pub struct GenesisConfig<T: Config> {193 pub invulnerables: Vec<T::AccountId>,194 }195196 #[cfg(feature = "std")]197 impl<T: Config> Default for GenesisConfig<T> {198 fn default() -> Self {199 Self {200 invulnerables: Default::default(),201 }202 }203 }204205 #[pallet::genesis_build]206 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {207 fn build(&self) {208 let duplicate_invulnerables = self209 .invulnerables210 .iter()211 .collect::<std::collections::BTreeSet<_>>();212 assert!(213 duplicate_invulnerables.len() == self.invulnerables.len(),214 "duplicate invulnerables in genesis."215 );216217 let bounded_invulnerables =218 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())219 .expect("genesis invulnerables are more than T::MaxCollators");220221 <Invulnerables<T>>::put(bounded_invulnerables);222 }223 }224225 #[pallet::event]226 #[pallet::generate_deposit(pub(super) fn deposit_event)]227 pub enum Event<T: Config> {228 InvulnerableAdded {229 invulnerable: T::AccountId,230 },231 InvulnerableRemoved {232 invulnerable: T::AccountId,233 },234 LicenseObtained {235 account_id: T::AccountId,236 deposit: BalanceOf<T>,237 },238 LicenseReleased {239 account_id: T::AccountId,240 deposit_returned: BalanceOf<T>,241 },242 CandidateAdded {243 account_id: T::AccountId,244 },245 CandidateRemoved {246 account_id: T::AccountId,247 },248 }249250 251 #[pallet::error]252 pub enum Error<T> {253 254 TooManyCandidates,255 256 Unknown,257 258 Permission,259 260 AlreadyHoldingLicense,261 262 NoLicense,263 264 AlreadyCandidate,265 266 NotCandidate,267 268 TooManyInvulnerables,269 270 TooFewInvulnerables,271 272 AlreadyInvulnerable,273 274 NotInvulnerable,275 276 NoAssociatedValidatorId,277 278 ValidatorNotRegistered,279 }280281 #[pallet::hooks]282 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}283284 #[pallet::call]285 impl<T: Config> Pallet<T> {286 287 #[pallet::call_index(0)]288 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]289 pub fn add_invulnerable(290 origin: OriginFor<T>,291 new: T::AccountId,292 ) -> DispatchResultWithPostInfo {293 T::UpdateOrigin::ensure_origin(origin)?;294295 296 let validator_key = T::ValidatorIdOf::convert(new.clone())297 .ok_or(Error::<T>::NoAssociatedValidatorId)?;298 ensure!(299 T::ValidatorRegistration::is_registered(&validator_key),300 Error::<T>::ValidatorNotRegistered301 );302 if Self::invulnerables().contains(&new) {303 return Ok(().into());304 }305306 <Invulnerables<T>>::try_append(new.clone())307 .map_err(|_| Error::<T>::TooManyInvulnerables)?;308309 310 let _ = Self::try_remove_candidate(&new);311312 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });313 Ok(().into())314 }315316 317 #[pallet::call_index(1)]318 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]319 pub fn remove_invulnerable(320 origin: OriginFor<T>,321 who: T::AccountId,322 ) -> DispatchResultWithPostInfo {323 T::UpdateOrigin::ensure_origin(origin)?;324325 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {326 if invulnerables.len() <= 1 {327 return Err(Error::<T>::TooFewInvulnerables.into());328 }329330 let index = invulnerables331 .into_iter()332 .position(|r| *r == who)333 .ok_or(Error::<T>::NotInvulnerable)?;334 invulnerables.remove(index);335 Ok(())336 })?;337 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });338 Ok(().into())339 }340341 342 343 344 345 346 #[pallet::call_index(2)]347 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]348 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {349 350 let who = ensure_signed(origin)?;351352 if LicenseDepositOf::<T>::contains_key(&who) {353 return Err(Error::<T>::AlreadyHoldingLicense.into());354 }355356 let validator_key = T::ValidatorIdOf::convert(who.clone())357 .ok_or(Error::<T>::NoAssociatedValidatorId)?;358 ensure!(359 T::ValidatorRegistration::is_registered(&validator_key),360 Error::<T>::ValidatorNotRegistered361 );362363 let deposit = <LicenseBond<T>>::get();364365 T::Currency::reserve(&who, deposit)?;366 LicenseDepositOf::<T>::insert(who.clone(), deposit);367368 Self::deposit_event(Event::LicenseObtained {369 account_id: who,370 deposit,371 });372 Ok(().into()) 373 }374375 376 377 378 379 #[pallet::call_index(3)]380 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]381 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {382 383 let who = ensure_signed(origin)?;384385 386 ensure!(387 LicenseDepositOf::<T>::contains_key(&who),388 Error::<T>::NoLicense389 );390 391 let length = <Candidates<T>>::decode_len().unwrap_or_default()392 + <Invulnerables<T>>::decode_len().unwrap_or_default();393 ensure!(394 (length as u32) < <DesiredCollators<T>>::get(),395 Error::<T>::TooManyCandidates396 );397 ensure!(398 !Self::invulnerables().contains(&who),399 Error::<T>::AlreadyInvulnerable400 );401402 let current_count =403 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {404 if candidates.iter().any(|candidate| *candidate == who) {405 Err(Error::<T>::AlreadyCandidate)?406 } else {407 candidates408 .try_push(who.clone())409 .map_err(|_| Error::<T>::TooManyCandidates)?;410 411 <LastAuthoredBlock<T>>::insert(412 who.clone(),413 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),414 );415 Ok(candidates.len())416 }417 })?;418419 Self::deposit_event(Event::CandidateAdded { account_id: who });420 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())421 }422423 424 425 #[pallet::call_index(4)]426 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]427 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {428 429 let who = ensure_signed(origin)?;430 let current_count = Self::try_remove_candidate(&who)?;431432 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())433 }434435 436 437 438 #[pallet::call_index(5)]439 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]440 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {441 442 let who = ensure_signed(origin)?;443444 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;445446 Ok(Some(<T as Config>::WeightInfo::release_license(447 current_count as u32,448 ))449 .into())450 }451452 453 454 455 456 457 #[pallet::call_index(6)]458 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]459 pub fn force_release_license(460 origin: OriginFor<T>,461 who: T::AccountId,462 ) -> DispatchResultWithPostInfo {463 464 T::UpdateOrigin::ensure_origin(origin)?;465466 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;467468 Ok(Some(<T as Config>::WeightInfo::force_release_license(469 current_count as u32,470 ))471 .into())472 }473 }474475 impl<T: Config> Pallet<T> {476 477 pub fn account_id() -> T::AccountId {478 T::PotId::get().into_account_truncating()479 }480481 482 483 fn try_remove_candidate_and_release_license(484 who: &T::AccountId,485 should_slash: bool,486 ignore_if_not_candidate: bool,487 ) -> Result<usize, DispatchError> {488 let current_count = Self::try_remove_candidate(who);489 let current_count = if ignore_if_not_candidate490 && current_count == Err(Error::<T>::NotCandidate.into())491 {492 <Candidates<T>>::decode_len().unwrap_or_default()493 } else {494 current_count?495 };496 Self::try_release_license(who, should_slash)?;497 Ok(current_count)498 }499500 501 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {502 let current_count =503 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {504 let index = candidates505 .iter()506 .position(|candidate| *candidate == *who)507 .ok_or(Error::<T>::NotCandidate)?;508 candidates.remove(index);509 <LastAuthoredBlock<T>>::remove(who.clone());510 Ok(candidates.len())511 })?;512 Self::deposit_event(Event::CandidateRemoved {513 account_id: who.clone(),514 });515 Ok(current_count)516 }517518 519 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {520 let mut deposit_returned = BalanceOf::<T>::default();521 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {522 if let Some(deposit) = deposit.take() {523 if should_slash {524 let slashed = T::SlashRatio::get() * deposit;525 let remaining = deposit - slashed;526527 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);528 529 deposit_returned = remaining;530531 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);532 } else {533 534 deposit_returned = deposit;535 }536537 T::Currency::unreserve(who, deposit_returned);538 Ok(())539 } else {540 Err(Error::<T>::NoLicense.into())541 }542 })?;543 Self::deposit_event(Event::LicenseReleased {544 account_id: who.clone(),545 deposit_returned,546 });547 Ok(())548 }549550 551 552 553 pub fn assemble_collators(554 candidates: BoundedVec<T::AccountId, T::MaxCollators>,555 ) -> Vec<T::AccountId> {556 let mut collators = Self::invulnerables().to_vec();557 collators.extend(candidates);558 collators559 }560561 562 563 pub fn kick_stale_candidates(564 candidates: BoundedVec<T::AccountId, T::MaxCollators>,565 ) -> BoundedVec<T::AccountId, T::MaxCollators> {566 let now = frame_system::Pallet::<T>::block_number();567 let kick_threshold = <KickThreshold<T>>::get();568 candidates569 .into_iter()570 .filter_map(|c| {571 let last_block = <LastAuthoredBlock<T>>::get(c.clone());572 let since_last = now.saturating_sub(last_block);573 if since_last < kick_threshold {574 Some(c)575 } else {576 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);577 if let Err(why) = outcome {578 log::warn!("Failed to kick collator and release license {:?}", why);579 debug_assert!(false, "failed to kick collator and release license {why:?}");580 }581 None582 }583 })584 .collect::<Vec<_>>()585 .try_into()586 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")587 }588 }589590 591 592 impl<T: Config + pallet_authorship::Config>593 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>594 {595 fn note_author(author: T::AccountId) {596 let pot = Self::account_id();597 598 let reward = T::Currency::free_balance(&pot)599 .checked_sub(&T::Currency::minimum_balance())600 .unwrap_or_else(Zero::zero)601 .div(2u32.into());602 603 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);604 debug_assert!(_success.is_ok());605 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());606607 frame_system::Pallet::<T>::register_extra_weight_unchecked(608 <T as Config>::WeightInfo::note_author(),609 DispatchClass::Mandatory,610 );611 }612 }613614 615 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {616 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {617 log::info!(618 "assembling new collators for new session {} at #{:?}",619 index,620 <frame_system::Pallet<T>>::block_number(),621 );622623 let candidates = Self::candidates();624 let candidates_len_before = candidates.len();625 let active_candidates = Self::kick_stale_candidates(candidates);626 let removed = candidates_len_before - active_candidates.len();627 let result = Self::assemble_collators(active_candidates);628629 frame_system::Pallet::<T>::register_extra_weight_unchecked(630 <T as Config>::WeightInfo::new_session(631 candidates_len_before as u32,632 removed as u32,633 ),634 DispatchClass::Mandatory,635 );636 Some(result)637 }638 fn start_session(_: SessionIndex) {639 640 }641 fn end_session(_: SessionIndex) {642 643 }644 }645}