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;9293use frame_support::traits::fungible::Inspect;9495type BalanceOf<T> =96 <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;97#[frame_support::pallet]98pub mod pallet {99 use super::*;100 pub use crate::weights::WeightInfo;101 use core::ops::Div;102 use frame_support::{103 dispatch::{DispatchClass, DispatchResultWithPostInfo},104 inherent::Vec,105 pallet_prelude::*,106 sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},107 traits::{108 EnsureOrigin,109 fungible::{Balanced, BalancedHold, Inspect, InspectHold, Mutate, MutateHold},110 ValidatorRegistration,111 tokens::{Precision, Preservation},112 },113 BoundedVec, PalletId,114 };115 use frame_system::pallet_prelude::*;116 use pallet_session::SessionManager;117 use sp_runtime::{Perbill, traits::Convert};118 use sp_staking::SessionIndex;119120 121 122 pub struct IdentityCollator;123 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {124 fn convert(t: T) -> Option<T> {125 Some(t)126 }127 }128129 130 #[pallet::config]131 pub trait Config: frame_system::Config {132 133 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;134 135 type RuntimeHoldReason: From<HoldReason>;136137 type Currency: Mutate<Self::AccountId>138 + MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>139 + BalancedHold<Self::AccountId>;140141 142 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;143144 145 type TreasuryAccountId: Get<Self::AccountId>;146147 148 type PotId: Get<PalletId>;149150 151 type MaxCollators: Get<u32>;152153 154 type SlashRatio: Get<Perbill>;155156 157 type ValidatorId: Member + Parameter;158159 160 161 162 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;163164 165 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;166167 168 type WeightInfo: WeightInfo;169170 type DesiredCollators: Get<u32>;171172 type LicenseBond: Get<BalanceOf<Self>>;173174 type KickThreshold: Get<BlockNumberFor<Self>>;175 }176177 #[pallet::composite_enum]178 pub enum HoldReason {179 180 LicenseBond,181 }182183 #[pallet::pallet]184 pub struct Pallet<T>(_);185186 187 #[pallet::storage]188 #[pallet::getter(fn invulnerables)]189 pub type Invulnerables<T: Config> =190 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;191192 193 #[pallet::storage]194 #[pallet::getter(fn license_deposit_of)]195 pub type LicenseDepositOf<T: Config> =196 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;197198 199 #[pallet::storage]200 #[pallet::getter(fn candidates)]201 pub type Candidates<T: Config> =202 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;203204 205 #[pallet::storage]206 #[pallet::getter(fn last_authored_block)]207 pub type LastAuthoredBlock<T: Config> =208 StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor<T>, ValueQuery>;209210 #[pallet::genesis_config]211 pub struct GenesisConfig<T: Config> {212 pub invulnerables: Vec<T::AccountId>,213 }214215 impl<T: Config> Default for GenesisConfig<T> {216 fn default() -> Self {217 Self {218 invulnerables: Default::default(),219 }220 }221 }222223 #[pallet::genesis_build]224 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {225 fn build(&self) {226 use sp_std::collections::btree_set::BTreeSet;227228 let duplicate_invulnerables = self.invulnerables.iter().collect::<BTreeSet<_>>();229 assert!(230 duplicate_invulnerables.len() == self.invulnerables.len(),231 "duplicate invulnerables in genesis."232 );233234 let bounded_invulnerables =235 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())236 .expect("genesis invulnerables are more than T::MaxCollators");237238 <Invulnerables<T>>::put(bounded_invulnerables);239 }240 }241242 #[pallet::event]243 #[pallet::generate_deposit(pub(super) fn deposit_event)]244 pub enum Event<T: Config> {245 InvulnerableAdded {246 invulnerable: T::AccountId,247 },248 InvulnerableRemoved {249 invulnerable: T::AccountId,250 },251 LicenseObtained {252 account_id: T::AccountId,253 deposit: BalanceOf<T>,254 },255 LicenseReleased {256 account_id: T::AccountId,257 deposit_returned: BalanceOf<T>,258 },259 CandidateAdded {260 account_id: T::AccountId,261 },262 CandidateRemoved {263 account_id: T::AccountId,264 },265 }266267 268 #[pallet::error]269 pub enum Error<T> {270 271 TooManyCandidates,272 273 Unknown,274 275 Permission,276 277 AlreadyHoldingLicense,278 279 NoLicense,280 281 AlreadyCandidate,282 283 NotCandidate,284 285 TooManyInvulnerables,286 287 TooFewInvulnerables,288 289 AlreadyInvulnerable,290 291 NotInvulnerable,292 293 NoAssociatedValidatorId,294 295 ValidatorNotRegistered,296 }297298 #[pallet::hooks]299 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}300301 #[pallet::call]302 impl<T: Config> Pallet<T> {303 304 #[pallet::call_index(0)]305 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]306 pub fn add_invulnerable(307 origin: OriginFor<T>,308 new: T::AccountId,309 ) -> DispatchResultWithPostInfo {310 T::UpdateOrigin::ensure_origin(origin)?;311312 313 let validator_key = T::ValidatorIdOf::convert(new.clone())314 .ok_or(Error::<T>::NoAssociatedValidatorId)?;315 ensure!(316 T::ValidatorRegistration::is_registered(&validator_key),317 Error::<T>::ValidatorNotRegistered318 );319 if Self::invulnerables().contains(&new) {320 return Ok(().into());321 }322323 <Invulnerables<T>>::try_append(new.clone())324 .map_err(|_| Error::<T>::TooManyInvulnerables)?;325326 327 let _ = Self::try_remove_candidate(&new);328329 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });330 Ok(().into())331 }332333 334 #[pallet::call_index(1)]335 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]336 pub fn remove_invulnerable(337 origin: OriginFor<T>,338 who: T::AccountId,339 ) -> DispatchResultWithPostInfo {340 T::UpdateOrigin::ensure_origin(origin)?;341342 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {343 if invulnerables.len() <= 1 {344 return Err(Error::<T>::TooFewInvulnerables.into());345 }346347 let index = invulnerables348 .into_iter()349 .position(|r| *r == who)350 .ok_or(Error::<T>::NotInvulnerable)?;351 invulnerables.remove(index);352 Ok(())353 })?;354 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });355 Ok(().into())356 }357358 359 360 361 362 363 #[pallet::call_index(2)]364 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]365 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {366 367 let who = ensure_signed(origin)?;368369 if LicenseDepositOf::<T>::contains_key(&who) {370 return Err(Error::<T>::AlreadyHoldingLicense.into());371 }372373 let validator_key = T::ValidatorIdOf::convert(who.clone())374 .ok_or(Error::<T>::NoAssociatedValidatorId)?;375 ensure!(376 T::ValidatorRegistration::is_registered(&validator_key),377 Error::<T>::ValidatorNotRegistered378 );379380 let deposit = T::LicenseBond::get();381382 T::Currency::hold(&HoldReason::LicenseBond.into(), &who, deposit)?;383 LicenseDepositOf::<T>::insert(who.clone(), deposit);384385 Self::deposit_event(Event::LicenseObtained {386 account_id: who,387 deposit,388 });389 Ok(().into()) 390 }391392 393 394 395 396 #[pallet::call_index(3)]397 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]398 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {399 400 let who = ensure_signed(origin)?;401402 403 ensure!(404 LicenseDepositOf::<T>::contains_key(&who),405 Error::<T>::NoLicense406 );407 408 let length = <Candidates<T>>::decode_len().unwrap_or_default()409 + <Invulnerables<T>>::decode_len().unwrap_or_default();410 ensure!(411 (length as u32) < T::DesiredCollators::get(),412 Error::<T>::TooManyCandidates413 );414 ensure!(415 !Self::invulnerables().contains(&who),416 Error::<T>::AlreadyInvulnerable417 );418419 let current_count =420 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {421 if candidates.iter().any(|candidate| *candidate == who) {422 Err(Error::<T>::AlreadyCandidate)?423 } else {424 candidates425 .try_push(who.clone())426 .map_err(|_| Error::<T>::TooManyCandidates)?;427 428 <LastAuthoredBlock<T>>::insert(429 who.clone(),430 frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),431 );432 Ok(candidates.len())433 }434 })?;435436 Self::deposit_event(Event::CandidateAdded { account_id: who });437 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())438 }439440 441 442 #[pallet::call_index(4)]443 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]444 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {445 446 let who = ensure_signed(origin)?;447 let current_count = Self::try_remove_candidate(&who)?;448449 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())450 }451452 453 454 455 #[pallet::call_index(5)]456 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]457 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {458 459 let who = ensure_signed(origin)?;460461 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;462463 Ok(Some(<T as Config>::WeightInfo::release_license(464 current_count as u32,465 ))466 .into())467 }468469 470 471 472 473 474 #[pallet::call_index(6)]475 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]476 pub fn force_release_license(477 origin: OriginFor<T>,478 who: T::AccountId,479 ) -> DispatchResultWithPostInfo {480 481 T::UpdateOrigin::ensure_origin(origin)?;482483 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;484485 Ok(Some(<T as Config>::WeightInfo::force_release_license(486 current_count as u32,487 ))488 .into())489 }490 }491492 impl<T: Config> Pallet<T> {493 494 pub fn account_id() -> T::AccountId {495 T::PotId::get().into_account_truncating()496 }497498 499 500 fn try_remove_candidate_and_release_license(501 who: &T::AccountId,502 should_slash: bool,503 ignore_if_not_candidate: bool,504 ) -> Result<usize, DispatchError> {505 let current_count = Self::try_remove_candidate(who);506 let current_count = if ignore_if_not_candidate507 && current_count == Err(Error::<T>::NotCandidate.into())508 {509 <Candidates<T>>::decode_len().unwrap_or_default()510 } else {511 current_count?512 };513 Self::try_release_license(who, should_slash)?;514 Ok(current_count)515 }516517 518 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {519 let current_count =520 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {521 let index = candidates522 .iter()523 .position(|candidate| *candidate == *who)524 .ok_or(Error::<T>::NotCandidate)?;525 candidates.remove(index);526 <LastAuthoredBlock<T>>::remove(who.clone());527 Ok(candidates.len())528 })?;529 Self::deposit_event(Event::CandidateRemoved {530 account_id: who.clone(),531 });532 Ok(current_count)533 }534535 536 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {537 let mut deposit_returned = BalanceOf::<T>::default();538 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {539 if let Some(deposit) = deposit.take() {540 if should_slash {541 let slashed = T::SlashRatio::get() * deposit;542 let remaining = deposit - slashed;543544 let (imbalance, _) =545 T::Currency::slash(&HoldReason::LicenseBond.into(), who, slashed);546 deposit_returned = remaining;547548 T::Currency::resolve(&T::TreasuryAccountId::get(), imbalance)549 .map_err(|_| DispatchError::Other("Failed to deposit imbalance"))?;550 } else {551 deposit_returned = deposit;552 }553554 T::Currency::release(555 &HoldReason::LicenseBond.into(),556 who,557 deposit_returned,558 Precision::Exact,559 )?;560 Ok(())561 } else {562 Err(Error::<T>::NoLicense.into())563 }564 })?;565 Self::deposit_event(Event::LicenseReleased {566 account_id: who.clone(),567 deposit_returned,568 });569 Ok(())570 }571572 573 574 575 pub fn assemble_collators(576 candidates: BoundedVec<T::AccountId, T::MaxCollators>,577 ) -> Vec<T::AccountId> {578 let mut collators = Self::invulnerables().to_vec();579 collators.extend(candidates);580 collators581 }582583 584 585 pub fn kick_stale_candidates(586 candidates: BoundedVec<T::AccountId, T::MaxCollators>,587 ) -> BoundedVec<T::AccountId, T::MaxCollators> {588 let now = frame_system::Pallet::<T>::block_number();589 let kick_threshold = T::KickThreshold::get();590 candidates591 .into_iter()592 .filter_map(|c| {593 let last_block = <LastAuthoredBlock<T>>::get(c.clone());594 let since_last = now.saturating_sub(last_block);595 if since_last < kick_threshold {596 Some(c)597 } else {598 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);599 if let Err(why) = outcome {600 log::warn!("Failed to kick collator and release license {:?}", why);601 debug_assert!(false, "failed to kick collator and release license {why:?}");602 }603 None604 }605 })606 .collect::<Vec<_>>()607 .try_into()608 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")609 }610 }611612 613 614 impl<T: Config + pallet_authorship::Config>615 pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T>616 {617 fn note_author(author: T::AccountId) {618 let pot = Self::account_id();619 620 let reward = T::Currency::balance(&pot)621 .checked_sub(&T::Currency::minimum_balance())622 .unwrap_or_else(Zero::zero)623 .div(2u32.into());624625 if !reward.is_zero() {626 627 let _success = T::Currency::transfer(&pot, &author, reward, Preservation::Preserve);628 debug_assert!(_success.is_ok());629 }630 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());631632 frame_system::Pallet::<T>::register_extra_weight_unchecked(633 <T as Config>::WeightInfo::note_author(),634 DispatchClass::Mandatory,635 );636 }637 }638639 640 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {641 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {642 log::info!(643 "assembling new collators for new session {} at #{:?}",644 index,645 <frame_system::Pallet<T>>::block_number(),646 );647648 let candidates = Self::candidates();649 let candidates_len_before = candidates.len();650 let active_candidates = Self::kick_stale_candidates(candidates);651 let removed = candidates_len_before - active_candidates.len();652 let result = Self::assemble_collators(active_candidates);653654 frame_system::Pallet::<T>::register_extra_weight_unchecked(655 <T as Config>::WeightInfo::new_session(656 candidates_len_before as u32,657 removed as u32,658 ),659 DispatchClass::Mandatory,660 );661 Some(result)662 }663 fn start_session(_: SessionIndex) {664 665 }666 fn end_session(_: SessionIndex) {667 668 }669 }670}