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 pub struct Pallet<T>(_);165166 167 #[pallet::storage]168 #[pallet::getter(fn invulnerables)]169 pub type Invulnerables<T: Config> =170 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;171172 173 #[pallet::storage]174 #[pallet::getter(fn license_deposit_of)]175 pub type LicenseDepositOf<T: Config> =176 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;177178 179 #[pallet::storage]180 #[pallet::getter(fn candidates)]181 pub type Candidates<T: Config> =182 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;183184 185 #[pallet::storage]186 #[pallet::getter(fn last_authored_block)]187 pub type LastAuthoredBlock<T: Config> =188 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;189190 #[pallet::genesis_config]191 pub struct GenesisConfig<T: Config> {192 pub invulnerables: Vec<T::AccountId>,193 }194195 #[cfg(feature = "std")]196 impl<T: Config> Default for GenesisConfig<T> {197 fn default() -> Self {198 Self {199 invulnerables: Default::default(),200 }201 }202 }203204 #[pallet::genesis_build]205 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {206 fn build(&self) {207 let duplicate_invulnerables = self208 .invulnerables209 .iter()210 .collect::<std::collections::BTreeSet<_>>();211 assert!(212 duplicate_invulnerables.len() == self.invulnerables.len(),213 "duplicate invulnerables in genesis."214 );215216 let bounded_invulnerables =217 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())218 .expect("genesis invulnerables are more than T::MaxCollators");219220 <Invulnerables<T>>::put(bounded_invulnerables);221 }222 }223224 #[pallet::event]225 #[pallet::generate_deposit(pub(super) fn deposit_event)]226 pub enum Event<T: Config> {227 InvulnerableAdded {228 invulnerable: T::AccountId,229 },230 InvulnerableRemoved {231 invulnerable: T::AccountId,232 },233 LicenseObtained {234 account_id: T::AccountId,235 deposit: BalanceOf<T>,236 },237 LicenseReleased {238 account_id: T::AccountId,239 deposit_returned: BalanceOf<T>,240 },241 CandidateAdded {242 account_id: T::AccountId,243 },244 CandidateRemoved {245 account_id: T::AccountId,246 },247 }248249 250 #[pallet::error]251 pub enum Error<T> {252 253 TooManyCandidates,254 255 Unknown,256 257 Permission,258 259 AlreadyHoldingLicense,260 261 NoLicense,262 263 AlreadyCandidate,264 265 NotCandidate,266 267 TooManyInvulnerables,268 269 TooFewInvulnerables,270 271 AlreadyInvulnerable,272 273 NotInvulnerable,274 275 NoAssociatedValidatorId,276 277 ValidatorNotRegistered,278 }279280 #[pallet::hooks]281 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}282283 #[pallet::call]284 impl<T: Config> Pallet<T> {285 286 #[pallet::call_index(0)]287 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]288 pub fn add_invulnerable(289 origin: OriginFor<T>,290 new: T::AccountId,291 ) -> DispatchResultWithPostInfo {292 T::UpdateOrigin::ensure_origin(origin)?;293294 295 let validator_key = T::ValidatorIdOf::convert(new.clone())296 .ok_or(Error::<T>::NoAssociatedValidatorId)?;297 ensure!(298 T::ValidatorRegistration::is_registered(&validator_key),299 Error::<T>::ValidatorNotRegistered300 );301 if Self::invulnerables().contains(&new) {302 return Ok(().into());303 }304305 <Invulnerables<T>>::try_append(new.clone())306 .map_err(|_| Error::<T>::TooManyInvulnerables)?;307308 309 let _ = Self::try_remove_candidate(&new);310311 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });312 Ok(().into())313 }314315 316 #[pallet::call_index(1)]317 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]318 pub fn remove_invulnerable(319 origin: OriginFor<T>,320 who: T::AccountId,321 ) -> DispatchResultWithPostInfo {322 T::UpdateOrigin::ensure_origin(origin)?;323324 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {325 if invulnerables.len() <= 1 {326 return Err(Error::<T>::TooFewInvulnerables.into());327 }328329 let index = invulnerables330 .into_iter()331 .position(|r| *r == who)332 .ok_or(Error::<T>::NotInvulnerable)?;333 invulnerables.remove(index);334 Ok(())335 })?;336 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });337 Ok(().into())338 }339340 341 342 343 344 345 #[pallet::call_index(2)]346 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]347 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {348 349 let who = ensure_signed(origin)?;350351 if LicenseDepositOf::<T>::contains_key(&who) {352 return Err(Error::<T>::AlreadyHoldingLicense.into());353 }354355 let validator_key = T::ValidatorIdOf::convert(who.clone())356 .ok_or(Error::<T>::NoAssociatedValidatorId)?;357 ensure!(358 T::ValidatorRegistration::is_registered(&validator_key),359 Error::<T>::ValidatorNotRegistered360 );361362 let deposit = <LicenseBond<T>>::get();363364 T::Currency::reserve(&who, deposit)?;365 LicenseDepositOf::<T>::insert(who.clone(), deposit);366367 Self::deposit_event(Event::LicenseObtained {368 account_id: who,369 deposit,370 });371 Ok(().into()) 372 }373374 375 376 377 378 #[pallet::call_index(3)]379 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]380 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {381 382 let who = ensure_signed(origin)?;383384 385 ensure!(386 LicenseDepositOf::<T>::contains_key(&who),387 Error::<T>::NoLicense388 );389 390 let length = <Candidates<T>>::decode_len().unwrap_or_default()391 + <Invulnerables<T>>::decode_len().unwrap_or_default();392 ensure!(393 (length as u32) < <DesiredCollators<T>>::get(),394 Error::<T>::TooManyCandidates395 );396 ensure!(397 !Self::invulnerables().contains(&who),398 Error::<T>::AlreadyInvulnerable399 );400401 let current_count =402 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {403 if candidates.iter().any(|candidate| *candidate == who) {404 Err(Error::<T>::AlreadyCandidate)?405 } else {406 candidates407 .try_push(who.clone())408 .map_err(|_| Error::<T>::TooManyCandidates)?;409 410 <LastAuthoredBlock<T>>::insert(411 who.clone(),412 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),413 );414 Ok(candidates.len())415 }416 })?;417418 Self::deposit_event(Event::CandidateAdded { account_id: who });419 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())420 }421422 423 424 #[pallet::call_index(4)]425 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]426 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {427 428 let who = ensure_signed(origin)?;429 let current_count = Self::try_remove_candidate(&who)?;430431 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())432 }433434 435 436 437 #[pallet::call_index(5)]438 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]439 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {440 441 let who = ensure_signed(origin)?;442443 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;444445 Ok(Some(<T as Config>::WeightInfo::release_license(446 current_count as u32,447 ))448 .into())449 }450451 452 453 454 455 456 #[pallet::call_index(6)]457 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]458 pub fn force_release_license(459 origin: OriginFor<T>,460 who: T::AccountId,461 ) -> DispatchResultWithPostInfo {462 463 T::UpdateOrigin::ensure_origin(origin)?;464465 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;466467 Ok(Some(<T as Config>::WeightInfo::force_release_license(468 current_count as u32,469 ))470 .into())471 }472 }473474 impl<T: Config> Pallet<T> {475 476 pub fn account_id() -> T::AccountId {477 T::PotId::get().into_account_truncating()478 }479480 481 482 fn try_remove_candidate_and_release_license(483 who: &T::AccountId,484 should_slash: bool,485 ignore_if_not_candidate: bool,486 ) -> Result<usize, DispatchError> {487 let current_count = Self::try_remove_candidate(who);488 let current_count = if ignore_if_not_candidate489 && current_count == Err(Error::<T>::NotCandidate.into())490 {491 <Candidates<T>>::decode_len().unwrap_or_default()492 } else {493 current_count?494 };495 Self::try_release_license(who, should_slash)?;496 Ok(current_count)497 }498499 500 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {501 let current_count =502 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {503 let index = candidates504 .iter()505 .position(|candidate| *candidate == *who)506 .ok_or(Error::<T>::NotCandidate)?;507 candidates.remove(index);508 <LastAuthoredBlock<T>>::remove(who.clone());509 Ok(candidates.len())510 })?;511 Self::deposit_event(Event::CandidateRemoved {512 account_id: who.clone(),513 });514 Ok(current_count)515 }516517 518 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {519 let mut deposit_returned = BalanceOf::<T>::default();520 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {521 if let Some(deposit) = deposit.take() {522 if should_slash {523 let slashed = T::SlashRatio::get() * deposit;524 let remaining = deposit - slashed;525526 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);527 528 deposit_returned = remaining;529530 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);531 } else {532 533 deposit_returned = deposit;534 }535536 T::Currency::unreserve(who, deposit_returned);537 Ok(())538 } else {539 Err(Error::<T>::NoLicense.into())540 }541 })?;542 Self::deposit_event(Event::LicenseReleased {543 account_id: who.clone(),544 deposit_returned,545 });546 Ok(())547 }548549 550 551 552 pub fn assemble_collators(553 candidates: BoundedVec<T::AccountId, T::MaxCollators>,554 ) -> Vec<T::AccountId> {555 let mut collators = Self::invulnerables().to_vec();556 collators.extend(candidates);557 collators558 }559560 561 562 pub fn kick_stale_candidates(563 candidates: BoundedVec<T::AccountId, T::MaxCollators>,564 ) -> BoundedVec<T::AccountId, T::MaxCollators> {565 let now = frame_system::Pallet::<T>::block_number();566 let kick_threshold = <KickThreshold<T>>::get();567 candidates568 .into_iter()569 .filter_map(|c| {570 let last_block = <LastAuthoredBlock<T>>::get(c.clone());571 let since_last = now.saturating_sub(last_block);572 if since_last < kick_threshold {573 Some(c)574 } else {575 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);576 if let Err(why) = outcome {577 log::warn!("Failed to kick collator and release license {:?}", why);578 debug_assert!(false, "failed to kick collator and release license {why:?}");579 }580 None581 }582 })583 .collect::<Vec<_>>()584 .try_into()585 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")586 }587 }588589 590 591 impl<T: Config + pallet_authorship::Config>592 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>593 {594 fn note_author(author: T::AccountId) {595 let pot = Self::account_id();596 597 let reward = T::Currency::free_balance(&pot)598 .checked_sub(&T::Currency::minimum_balance())599 .unwrap_or_else(Zero::zero)600 .div(2u32.into());601 602 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);603 debug_assert!(_success.is_ok());604 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());605606 frame_system::Pallet::<T>::register_extra_weight_unchecked(607 <T as Config>::WeightInfo::note_author(),608 DispatchClass::Mandatory,609 );610 }611 }612613 614 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {615 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {616 log::info!(617 "assembling new collators for new session {} at #{:?}",618 index,619 <frame_system::Pallet<T>>::block_number(),620 );621622 let candidates = Self::candidates();623 let candidates_len_before = candidates.len();624 let active_candidates = Self::kick_stale_candidates(candidates);625 let removed = candidates_len_before - active_candidates.len();626 let result = Self::assemble_collators(active_candidates);627628 frame_system::Pallet::<T>::register_extra_weight_unchecked(629 <T as Config>::WeightInfo::new_session(630 candidates_len_before as u32,631 removed as u32,632 ),633 DispatchClass::Mandatory,634 );635 Some(result)636 }637 fn start_session(_: SessionIndex) {638 639 }640 fn end_session(_: SessionIndex) {641 642 }643 }644}