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::{102 traits::{AccountIdConversion, CheckedSub, Saturating, Zero},103 RuntimeDebug,104 },105 traits::{106 Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,107 ValidatorRegistration,108 },109 BoundedVec, PalletId,110 };111 use frame_system::{pallet_prelude::*, Config as SystemConfig};112 use pallet_session::SessionManager;113 use sp_runtime::{114 Perbill,115 traits::{One, Convert},116 };117 use sp_staking::SessionIndex;118119 type BalanceOf<T> =120 <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;121122 123 124 pub struct IdentityCollator;125 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {126 fn convert(t: T) -> Option<T> {127 Some(t)128 }129 }130131 132 #[pallet::config]133 pub trait Config: frame_system::Config {134 135 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;136137 138 type Currency: ReservableCurrency<Self::AccountId>;139140 141 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;142143 144 type TreasuryAccountId: Get<Self::AccountId>;145146 147 type PotId: Get<PalletId>;148149 150 151 152 type MaxCandidates: Get<u32>;153154 155 156 157 type MinCandidates: Get<u32>;158159 160 type MaxInvulnerables: Get<u32>;161162 163 type SlashRatio: Get<Perbill>;164165 166 type ValidatorId: Member + Parameter;167168 169 170 171 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;172173 174 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;175176 177 type WeightInfo: WeightInfo;178 }179180 181 #[derive(182 PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,183 )]184 pub struct LicenseInfo<AccountId, Balance> {185 186 pub who: AccountId,187 188 pub deposit: Balance,189 }190191 #[pallet::pallet]192 #[pallet::generate_store(pub(super) trait Store)]193 pub struct Pallet<T>(_);194195 196 #[pallet::storage]197 #[pallet::getter(fn invulnerables)]198 pub type Invulnerables<T: Config> =199 StorageValue<_, BoundedVec<T::AccountId, T::MaxInvulnerables>, ValueQuery>;200201 202 #[pallet::storage]203 #[pallet::getter(fn licenses)]204 pub type Licenses<T: Config> =205 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;206207 208 #[pallet::storage]209 #[pallet::getter(fn candidates)]210 pub type Candidates<T: Config> = StorageValue<211 _,212 BoundedVec<T::AccountId, T::MaxCandidates>, 213 ValueQuery,214 >;215216 217 218 219 #[pallet::storage]220 #[pallet::getter(fn kick_threshold)]221 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;222223 224 #[pallet::storage]225 #[pallet::getter(fn last_authored_block)]226 pub type LastAuthoredBlock<T: Config> =227 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;228229 230 231 232 #[pallet::storage]233 #[pallet::getter(fn desired_candidates)]234 pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;235236 237 238 239 #[pallet::storage]240 #[pallet::getter(fn license_bond)]241 pub type LicenseBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;242243 #[pallet::genesis_config]244 pub struct GenesisConfig<T: Config> {245 pub invulnerables: Vec<T::AccountId>,246 pub license_bond: BalanceOf<T>,247 pub kick_threshold: T::BlockNumber,248 pub desired_candidates: u32,249 }250251 #[cfg(feature = "std")]252 impl<T: Config> Default for GenesisConfig<T> {253 fn default() -> Self {254 Self {255 invulnerables: Default::default(),256 license_bond: Default::default(),257 kick_threshold: T::BlockNumber::one(),258 desired_candidates: Default::default(),259 }260 }261 }262263 #[pallet::genesis_build]264 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {265 fn build(&self) {266 let duplicate_invulnerables = self267 .invulnerables268 .iter()269 .collect::<std::collections::BTreeSet<_>>();270 assert!(271 duplicate_invulnerables.len() == self.invulnerables.len(),272 "duplicate invulnerables in genesis."273 );274275 let bounded_invulnerables =276 BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())277 .expect("genesis invulnerables are more than T::MaxInvulnerables");278 assert!(279 T::MaxCandidates::get() >= self.desired_candidates,280 "genesis desired_candidates are more than T::MaxCandidates",281 );282283 <DesiredCandidates<T>>::put(&self.desired_candidates);284 <LicenseBond<T>>::put(&self.license_bond);285 <KickThreshold<T>>::put(&self.kick_threshold);286 <Invulnerables<T>>::put(bounded_invulnerables);287 }288 }289290 #[pallet::event]291 #[pallet::generate_deposit(pub(super) fn deposit_event)]292 pub enum Event<T: Config> {293 NewDesiredCandidates {294 desired_candidates: u32,295 },296 NewLicenseBond {297 bond_amount: BalanceOf<T>,298 },299 NewKickThreshold {300 length_in_blocks: T::BlockNumber,301 },302 InvulnerableAdded {303 invulnerable: T::AccountId,304 },305 InvulnerableRemoved {306 invulnerable: T::AccountId,307 },308 LicenseObtained {309 account_id: T::AccountId,310 deposit: BalanceOf<T>,311 },312 LicenseForfeited {313 account_id: T::AccountId,314 deposit_returned: BalanceOf<T>,315 },316 CandidateAdded {317 account_id: T::AccountId,318 },319 CandidateRemoved {320 account_id: T::AccountId,321 },322 }323324 325 #[pallet::error]326 pub enum Error<T> {327 328 TooManyCandidates,329 330 TooFewCandidates,331 332 Unknown,333 334 Permission,335 336 AlreadyLicenseHolder,337 338 NoLicense,339 340 AlreadyCandidate,341 342 NotCandidate,343 344 TooManyInvulnerables,345 346 TooFewInvulnerables,347 348 AlreadyInvulnerable,349 350 NotInvulnerable,351 352 NoAssociatedValidatorId,353 354 ValidatorNotRegistered,355 }356357 #[pallet::hooks]358 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}359360 #[pallet::call]361 impl<T: Config> Pallet<T> {362 363 #[pallet::weight(T::WeightInfo::set_invulnerables(1 as u32))] 364 pub fn add_invulnerable(365 origin: OriginFor<T>,366 new: T::AccountId,367 ) -> DispatchResultWithPostInfo {368 T::UpdateOrigin::ensure_origin(origin)?;369370 371 let validator_key = T::ValidatorIdOf::convert(new.clone())372 .ok_or(Error::<T>::NoAssociatedValidatorId)?;373 ensure!(374 T::ValidatorRegistration::is_registered(&validator_key),375 Error::<T>::ValidatorNotRegistered376 );377 378 if Self::invulnerables().contains(&new) {379 return Ok(().into());380 }381382 383 384385 <Invulnerables<T>>::try_append(new.clone())386 .map_err(|_| Error::<T>::TooManyInvulnerables)?;387 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });388 Ok(().into())389 }390391 392 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] 393 pub fn remove_invulnerable(394 origin: OriginFor<T>,395 who: T::AccountId,396 ) -> DispatchResultWithPostInfo {397 T::UpdateOrigin::ensure_origin(origin)?;398399 400 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {401 if invulnerables.len() <= 1 {402 return Err(Error::<T>::TooFewInvulnerables.into());403 }404405 let index = invulnerables406 .into_iter()407 .position(|r| *r == who)408 .ok_or(Error::<T>::NotInvulnerable)?;409 invulnerables.remove(index);410 Ok(())411 })?;412 413414415416 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });417 Ok(().into())418 }419420 421 422 423 #[pallet::weight(T::WeightInfo::set_desired_candidates())]424 pub fn set_desired_candidates(425 origin: OriginFor<T>,426 max: u32,427 ) -> DispatchResultWithPostInfo {428 T::UpdateOrigin::ensure_origin(origin)?;429 430 if max > T::MaxCandidates::get() {431 log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");432 }433 <DesiredCandidates<T>>::put(&max);434 Self::deposit_event(Event::NewDesiredCandidates {435 desired_candidates: max,436 });437 Ok(().into())438 }439440 441 #[pallet::weight(T::WeightInfo::set_license_bond())]442 pub fn set_license_bond(443 origin: OriginFor<T>,444 bond: BalanceOf<T>,445 ) -> DispatchResultWithPostInfo {446 T::UpdateOrigin::ensure_origin(origin)?;447 <LicenseBond<T>>::put(&bond);448 Self::deposit_event(Event::NewLicenseBond { bond_amount: bond });449 Ok(().into())450 }451452 453 454 #[pallet::weight(T::WeightInfo::set_license_bond())] 455 pub fn set_kick_threshold(456 origin: OriginFor<T>,457 kick_threshold: T::BlockNumber,458 ) -> DispatchResultWithPostInfo {459 T::UpdateOrigin::ensure_origin(origin)?;460 461 <KickThreshold<T>>::put(kick_threshold);462 Self::deposit_event(Event::NewKickThreshold {463 length_in_blocks: kick_threshold,464 });465 Ok(().into())466 }467468 469 470 471 472 473 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] 474 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {475 476 let who = ensure_signed(origin)?;477478 if Licenses::<T>::contains_key(&who) {479 return Ok(().into());480 }481482 ensure!(483 !Self::invulnerables().contains(&who),484 Error::<T>::AlreadyInvulnerable485 );486487 let validator_key = T::ValidatorIdOf::convert(who.clone())488 .ok_or(Error::<T>::NoAssociatedValidatorId)?;489 ensure!(490 T::ValidatorRegistration::is_registered(&validator_key),491 Error::<T>::ValidatorNotRegistered492 );493494 let deposit = Self::license_bond();495 496 497498499500501 T::Currency::reserve(&who, deposit)?;502 Licenses::<T>::insert(who.clone(), deposit);503504 505506507508509510511512513514515516517518519520521522523524 Self::deposit_event(Event::LicenseObtained {525 account_id: who,526 deposit,527 });528 Ok(().into()) 529 }530531 532 533 534 535 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] 536 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {537 538 let who = ensure_signed(origin)?;539540 541 ensure!(Licenses::<T>::contains_key(&who), Error::<T>::NoLicense);542 543 let length = <Candidates<T>>::decode_len().unwrap_or_default();544 ensure!(545 (length as u32) < Self::desired_candidates(),546 Error::<T>::TooManyCandidates547 );548 549 ensure!(550 !Self::invulnerables().contains(&who),551 Error::<T>::AlreadyInvulnerable552 );553554 let deposit = Self::license_bond();555 556 557558559560561 let current_count =562 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {563 if candidates.iter().any(|candidate| *candidate == who) {564 Err(Error::<T>::AlreadyCandidate)?565 } else {566 T::Currency::reserve(&who, deposit)?;567 candidates568 .try_push(who.clone())569 .map_err(|_| Error::<T>::TooManyCandidates)?;570 <LastAuthoredBlock<T>>::insert(571 who.clone(),572 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),573 );574 Ok(candidates.len())575 }576 })?;577578 Self::deposit_event(Event::CandidateAdded { account_id: who });579 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())580 }581582 583 584 585 586 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] 587 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {588 589 let who = ensure_signed(origin)?;590 591 ensure!(592 Self::candidates().len() as u32 > T::MinCandidates::get(),593 Error::<T>::TooFewCandidates594 );595 let current_count = Self::try_remove_candidate(&who)?;596597 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())598 }599600 601 602 603 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] 604 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {605 606 let who = ensure_signed(origin)?;607 608 Self::try_release_license(&who, false)?;609610 Ok(().into())611 }612613 614 615 616 617 618 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] 619 pub fn force_release_license(620 origin: OriginFor<T>,621 who: T::AccountId,622 ) -> DispatchResultWithPostInfo {623 624 T::UpdateOrigin::ensure_origin(origin)?;625626 let current_count = Self::try_remove_candidate(&who)?;627 Self::try_release_license(&who, false)?;628629 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 630 }631 }632633 impl<T: Config> Pallet<T> {634 635 pub fn account_id() -> T::AccountId {636 T::PotId::get().into_account_truncating()637 }638639 640 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {641 let current_count =642 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {643 let index = candidates644 .iter()645 .position(|candidate| *candidate == *who)646 .ok_or(Error::<T>::NotCandidate)?;647 candidates.remove(index);648 <LastAuthoredBlock<T>>::remove(who.clone());649 Ok(candidates.len())650 })?;651 Self::deposit_event(Event::CandidateRemoved {652 account_id: who.clone(),653 });654 Ok(current_count)655 }656657 658 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {659 let mut deposit_returned = BalanceOf::<T>::default();660 Licenses::<T>::try_mutate_exists(&who, |deposit| -> DispatchResult {661 if let Some(deposit) = deposit.take() {662 if should_slash {663 let slashed = T::SlashRatio::get() * deposit;664 let remaining = deposit - slashed;665666 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);667 668 deposit_returned = remaining;669670 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);671 } else {672 673 deposit_returned = deposit;674 }675676 T::Currency::unreserve(who, deposit_returned);677 Ok(())678 } else {679 Err(Error::<T>::NoLicense.into())680 }681 })?;682 Self::deposit_event(Event::LicenseForfeited {683 account_id: who.clone(),684 deposit_returned,685 });686 Ok(())687 }688689 690 691 692 pub fn assemble_collators(693 candidates: BoundedVec<T::AccountId, T::MaxCandidates>,694 ) -> Vec<T::AccountId> {695 let mut collators = Self::invulnerables().to_vec();696 collators.extend(candidates);697 collators698 }699700 701 702 pub fn kick_stale_candidates(703 candidates: BoundedVec<T::AccountId, T::MaxCandidates>, 704 ) -> BoundedVec<T::AccountId, T::MaxCandidates> {705 let now = frame_system::Pallet::<T>::block_number();706 let kick_threshold = Self::kick_threshold();707 candidates708 .into_iter()709 .filter_map(|c| {710 let last_block = <LastAuthoredBlock<T>>::get(c.clone());711 let since_last = now.saturating_sub(last_block);712 if since_last < kick_threshold ||713 Self::candidates().len() as u32 <= T::MinCandidates::get()714 {715 Some(c)716 } else {717 let outcome = Self::try_remove_candidate(&c);718 if let Err(why) = outcome {719 log::warn!("Failed to remove candidate {:?}", why);720 debug_assert!(false, "failed to remove candidate {:?}", why);721 return None;722 }723 let outcome = Self::try_release_license(&c, true);724 if let Err(why) = outcome {725 log::warn!("Failed to release license {:?}", why);726 debug_assert!(false, "failed to release license {:?}", why);727 }728 None729 }730 })731 .collect::<Vec<_>>()732 .try_into()733 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")734 }735 }736737 738 739 impl<T: Config + pallet_authorship::Config>740 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>741 {742 fn note_author(author: T::AccountId) {743 let pot = Self::account_id();744 745 let reward = T::Currency::free_balance(&pot)746 .checked_sub(&T::Currency::minimum_balance())747 .unwrap_or_else(Zero::zero)748 .div(2u32.into());749 750 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);751 debug_assert!(_success.is_ok());752 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());753754 frame_system::Pallet::<T>::register_extra_weight_unchecked(755 T::WeightInfo::note_author(),756 DispatchClass::Mandatory,757 );758 }759760 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {761 762 }763 }764765 766 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {767 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {768 log::info!(769 "assembling new collators for new session {} at #{:?}",770 index,771 <frame_system::Pallet<T>>::block_number(),772 );773774 let candidates = Self::candidates();775 let candidates_len_before = candidates.len();776 let active_candidates = Self::kick_stale_candidates(candidates);777 let removed = candidates_len_before - active_candidates.len();778 let result = Self::assemble_collators(active_candidates);779780 frame_system::Pallet::<T>::register_extra_weight_unchecked(781 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),782 DispatchClass::Mandatory,783 );784 Some(result)785 }786 fn start_session(_: SessionIndex) {787 788 }789 fn end_session(_: SessionIndex) {790 791 }792 }793}