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");220 221 <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::weight(T::WeightInfo::set_invulnerables(1u32))] 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::weight(T::WeightInfo::set_invulnerables(1))] 317 pub fn remove_invulnerable(318 origin: OriginFor<T>,319 who: T::AccountId,320 ) -> DispatchResultWithPostInfo {321 T::UpdateOrigin::ensure_origin(origin)?;322323 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {324 if invulnerables.len() <= 1 {325 return Err(Error::<T>::TooFewInvulnerables.into());326 }327328 let index = invulnerables329 .into_iter()330 .position(|r| *r == who)331 .ok_or(Error::<T>::NotInvulnerable)?;332 invulnerables.remove(index);333 Ok(())334 })?;335 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });336 Ok(().into())337 }338339 340 341 342 343 344 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] 345 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {346 347 let who = ensure_signed(origin)?;348349 if LicenseDepositOf::<T>::contains_key(&who) {350 return Err(Error::<T>::AlreadyHoldingLicense.into());351 }352353 let validator_key = T::ValidatorIdOf::convert(who.clone())354 .ok_or(Error::<T>::NoAssociatedValidatorId)?;355 ensure!(356 T::ValidatorRegistration::is_registered(&validator_key),357 Error::<T>::ValidatorNotRegistered358 );359360 let deposit = <LicenseBond<T>>::get();361362 T::Currency::reserve(&who, deposit)?;363 LicenseDepositOf::<T>::insert(who.clone(), deposit);364365 Self::deposit_event(Event::LicenseObtained {366 account_id: who,367 deposit,368 });369 Ok(().into()) 370 }371372 373 374 375 376 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] 377 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {378 379 let who = ensure_signed(origin)?;380381 382 ensure!(383 LicenseDepositOf::<T>::contains_key(&who),384 Error::<T>::NoLicense385 );386 387 let length = <Candidates<T>>::decode_len().unwrap_or_default()388 + <Invulnerables<T>>::decode_len().unwrap_or_default();389 ensure!(390 (length as u32) < <DesiredCollators<T>>::get(),391 Error::<T>::TooManyCandidates392 );393 ensure!(394 !Self::invulnerables().contains(&who),395 Error::<T>::AlreadyInvulnerable396 );397398 let current_count =399 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {400 if candidates.iter().any(|candidate| *candidate == who) {401 Err(Error::<T>::AlreadyCandidate)?402 } else {403 candidates404 .try_push(who.clone())405 .map_err(|_| Error::<T>::TooManyCandidates)?;406 407 <LastAuthoredBlock<T>>::insert(408 who.clone(),409 frame_system::Pallet::<T>::block_number() + <KickThreshold<T>>::get(),410 );411 Ok(candidates.len())412 }413 })?;414415 Self::deposit_event(Event::CandidateAdded { account_id: who });416 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())417 }418419 420 421 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 422 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {423 424 let who = ensure_signed(origin)?;425 let current_count = Self::try_remove_candidate(&who)?;426427 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 428 }429430 431 432 433 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 434 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {435 436 let who = ensure_signed(origin)?;437438 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;439440 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 441 }442443 444 445 446 447 448 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 449 pub fn force_revoke_license(450 origin: OriginFor<T>,451 who: T::AccountId,452 ) -> DispatchResultWithPostInfo {453 454 T::UpdateOrigin::ensure_origin(origin)?;455456 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;457458 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 459 }460 }461462 impl<T: Config> Pallet<T> {463 464 pub fn account_id() -> T::AccountId {465 T::PotId::get().into_account_truncating()466 }467468 469 470 fn try_remove_candidate_and_release_license(471 who: &T::AccountId,472 should_slash: bool,473 ignore_if_not_candidate: bool,474 ) -> Result<usize, DispatchError> {475 let current_count = Self::try_remove_candidate(who);476 let current_count = if ignore_if_not_candidate477 && current_count == Err(Error::<T>::NotCandidate.into())478 {479 <Candidates<T>>::decode_len().unwrap_or_default()480 } else {481 current_count?482 };483 Self::try_release_license(who, should_slash)?;484 Ok(current_count)485 }486487 488 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {489 let current_count =490 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {491 let index = candidates492 .iter()493 .position(|candidate| *candidate == *who)494 .ok_or(Error::<T>::NotCandidate)?;495 candidates.remove(index);496 <LastAuthoredBlock<T>>::remove(who.clone());497 Ok(candidates.len())498 })?;499 Self::deposit_event(Event::CandidateRemoved {500 account_id: who.clone(),501 });502 Ok(current_count)503 }504505 506 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {507 let mut deposit_returned = BalanceOf::<T>::default();508 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {509 if let Some(deposit) = deposit.take() {510 if should_slash {511 let slashed = T::SlashRatio::get() * deposit;512 let remaining = deposit - slashed;513514 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);515 516 deposit_returned = remaining;517518 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);519 } else {520 521 deposit_returned = deposit;522 }523524 T::Currency::unreserve(who, deposit_returned);525 Ok(())526 } else {527 Err(Error::<T>::NoLicense.into())528 }529 })?;530 Self::deposit_event(Event::LicenseForfeited {531 account_id: who.clone(),532 deposit_returned,533 });534 Ok(())535 }536537 538 539 540 pub fn assemble_collators(541 candidates: BoundedVec<T::AccountId, T::MaxCollators>,542 ) -> Vec<T::AccountId> {543 let mut collators = Self::invulnerables().to_vec();544 collators.extend(candidates);545 collators546 }547548 549 550 pub fn kick_stale_candidates(551 candidates: BoundedVec<T::AccountId, T::MaxCollators>,552 ) -> BoundedVec<T::AccountId, T::MaxCollators> {553 let now = frame_system::Pallet::<T>::block_number();554 let kick_threshold = <KickThreshold<T>>::get();555 candidates556 .into_iter()557 .filter_map(|c| {558 let last_block = <LastAuthoredBlock<T>>::get(c.clone());559 let since_last = now.saturating_sub(last_block);560 if since_last < kick_threshold {561 Some(c)562 } else {563 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);564 if let Err(why) = outcome {565 log::warn!("Failed to kick collator and release license {:?}", why);566 debug_assert!(false, "failed to kick collator and release license {why:?}");567 }568 None569 }570 })571 .collect::<Vec<_>>()572 .try_into()573 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")574 }575 }576577 578 579 impl<T: Config + pallet_authorship::Config>580 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>581 {582 fn note_author(author: T::AccountId) {583 let pot = Self::account_id();584 585 let reward = T::Currency::free_balance(&pot)586 .checked_sub(&T::Currency::minimum_balance())587 .unwrap_or_else(Zero::zero)588 .div(2u32.into());589 590 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);591 debug_assert!(_success.is_ok());592 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());593594 frame_system::Pallet::<T>::register_extra_weight_unchecked(595 T::WeightInfo::note_author(),596 DispatchClass::Mandatory,597 );598 }599600 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {601 602 }603 }604605 606 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {607 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {608 log::info!(609 "assembling new collators for new session {} at #{:?}",610 index,611 <frame_system::Pallet<T>>::block_number(),612 );613614 let candidates = Self::candidates();615 let candidates_len_before = candidates.len();616 let active_candidates = Self::kick_stale_candidates(candidates);617 let removed = candidates_len_before - active_candidates.len();618 let result = Self::assemble_collators(active_candidates);619620 frame_system::Pallet::<T>::register_extra_weight_unchecked(621 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),622 DispatchClass::Mandatory,623 );624 Some(result)625 }626 fn start_session(_: SessionIndex) {627 628 }629 fn end_session(_: SessionIndex) {630 631 }632 }633}