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 LicenseForfeited {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::WeightInfo::set_invulnerables(1u32))] 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::WeightInfo::set_invulnerables(1))] 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::WeightInfo::register_as_candidate(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::WeightInfo::register_as_candidate(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::WeightInfo::register_as_candidate(current_count as u32)).into())421 }422423 424 425 #[pallet::call_index(4)]426 #[pallet::weight(T::WeightInfo::leave_intent(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::WeightInfo::leave_intent(current_count as u32)).into()) 433 }434435 436 437 438 #[pallet::call_index(5)]439 #[pallet::weight(T::WeightInfo::leave_intent(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::WeightInfo::leave_intent(current_count as u32)).into()) 447 }448449 450 451 452 453 454 #[pallet::call_index(6)]455 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 456 pub fn force_release_license(457 origin: OriginFor<T>,458 who: T::AccountId,459 ) -> DispatchResultWithPostInfo {460 461 T::UpdateOrigin::ensure_origin(origin)?;462463 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;464465 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 466 }467 }468469 impl<T: Config> Pallet<T> {470 471 pub fn account_id() -> T::AccountId {472 T::PotId::get().into_account_truncating()473 }474475 476 477 fn try_remove_candidate_and_release_license(478 who: &T::AccountId,479 should_slash: bool,480 ignore_if_not_candidate: bool,481 ) -> Result<usize, DispatchError> {482 let current_count = Self::try_remove_candidate(who);483 let current_count = if ignore_if_not_candidate484 && current_count == Err(Error::<T>::NotCandidate.into())485 {486 <Candidates<T>>::decode_len().unwrap_or_default()487 } else {488 current_count?489 };490 Self::try_release_license(who, should_slash)?;491 Ok(current_count)492 }493494 495 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {496 let current_count =497 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {498 let index = candidates499 .iter()500 .position(|candidate| *candidate == *who)501 .ok_or(Error::<T>::NotCandidate)?;502 candidates.remove(index);503 <LastAuthoredBlock<T>>::remove(who.clone());504 Ok(candidates.len())505 })?;506 Self::deposit_event(Event::CandidateRemoved {507 account_id: who.clone(),508 });509 Ok(current_count)510 }511512 513 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {514 let mut deposit_returned = BalanceOf::<T>::default();515 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {516 if let Some(deposit) = deposit.take() {517 if should_slash {518 let slashed = T::SlashRatio::get() * deposit;519 let remaining = deposit - slashed;520521 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);522 523 deposit_returned = remaining;524525 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);526 } else {527 528 deposit_returned = deposit;529 }530531 T::Currency::unreserve(who, deposit_returned);532 Ok(())533 } else {534 Err(Error::<T>::NoLicense.into())535 }536 })?;537 Self::deposit_event(Event::LicenseForfeited {538 account_id: who.clone(),539 deposit_returned,540 });541 Ok(())542 }543544 545 546 547 pub fn assemble_collators(548 candidates: BoundedVec<T::AccountId, T::MaxCollators>,549 ) -> Vec<T::AccountId> {550 let mut collators = Self::invulnerables().to_vec();551 collators.extend(candidates);552 collators553 }554555 556 557 pub fn kick_stale_candidates(558 candidates: BoundedVec<T::AccountId, T::MaxCollators>,559 ) -> BoundedVec<T::AccountId, T::MaxCollators> {560 let now = frame_system::Pallet::<T>::block_number();561 let kick_threshold = <KickThreshold<T>>::get();562 candidates563 .into_iter()564 .filter_map(|c| {565 let last_block = <LastAuthoredBlock<T>>::get(c.clone());566 let since_last = now.saturating_sub(last_block);567 if since_last < kick_threshold {568 Some(c)569 } else {570 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);571 if let Err(why) = outcome {572 log::warn!("Failed to kick collator and release license {:?}", why);573 debug_assert!(false, "failed to kick collator and release license {why:?}");574 }575 None576 }577 })578 .collect::<Vec<_>>()579 .try_into()580 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")581 }582 }583584 585 586 impl<T: Config + pallet_authorship::Config>587 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>588 {589 fn note_author(author: T::AccountId) {590 let pot = Self::account_id();591 592 let reward = T::Currency::free_balance(&pot)593 .checked_sub(&T::Currency::minimum_balance())594 .unwrap_or_else(Zero::zero)595 .div(2u32.into());596 597 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);598 debug_assert!(_success.is_ok());599 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());600601 frame_system::Pallet::<T>::register_extra_weight_unchecked(602 T::WeightInfo::note_author(),603 DispatchClass::Mandatory,604 );605 }606607 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {608 609 }610 }611612 613 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {614 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {615 log::info!(616 "assembling new collators for new session {} at #{:?}",617 index,618 <frame_system::Pallet<T>>::block_number(),619 );620621 let candidates = Self::candidates();622 let candidates_len_before = candidates.len();623 let active_candidates = Self::kick_stale_candidates(candidates);624 let removed = candidates_len_before - active_candidates.len();625 let result = Self::assemble_collators(active_candidates);626627 frame_system::Pallet::<T>::register_extra_weight_unchecked(628 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),629 DispatchClass::Mandatory,630 );631 Some(result)632 }633 fn start_session(_: SessionIndex) {634 635 }636 fn end_session(_: SessionIndex) {637 638 }639 }640}