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 type Currency: Mutate<Self::AccountId>135 + MutateHold<Self::AccountId>136 + BalancedHold<Self::AccountId>;137138 139 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;140141 142 type TreasuryAccountId: Get<Self::AccountId>;143144 145 type PotId: Get<PalletId>;146147 148 type MaxCollators: Get<u32>;149150 151 type SlashRatio: Get<Perbill>;152153 154 type ValidatorId: Member + Parameter;155156 157 158 159 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;160161 162 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;163164 165 type WeightInfo: WeightInfo;166167 #[pallet::constant]168 type LicenceBondIdentifier: Get<<Self::Currency as InspectHold<Self::AccountId>>::Reason>;169170 type DesiredCollators: Get<u32>;171172 type LicenseBond: Get<BalanceOf<Self>>;173174 type KickThreshold: Get<Self::BlockNumber>;175 }176177 #[pallet::pallet]178 pub struct Pallet<T>(_);179180 181 #[pallet::storage]182 #[pallet::getter(fn invulnerables)]183 pub type Invulnerables<T: Config> =184 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;185186 187 #[pallet::storage]188 #[pallet::getter(fn license_deposit_of)]189 pub type LicenseDepositOf<T: Config> =190 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;191192 193 #[pallet::storage]194 #[pallet::getter(fn candidates)]195 pub type Candidates<T: Config> =196 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;197198 199 #[pallet::storage]200 #[pallet::getter(fn last_authored_block)]201 pub type LastAuthoredBlock<T: Config> =202 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;203204 #[pallet::genesis_config]205 pub struct GenesisConfig<T: Config> {206 pub invulnerables: Vec<T::AccountId>,207 }208209 #[cfg(feature = "std")]210 impl<T: Config> Default for GenesisConfig<T> {211 fn default() -> Self {212 Self {213 invulnerables: Default::default(),214 }215 }216 }217218 #[pallet::genesis_build]219 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {220 fn build(&self) {221 let duplicate_invulnerables = self222 .invulnerables223 .iter()224 .collect::<std::collections::BTreeSet<_>>();225 assert!(226 duplicate_invulnerables.len() == self.invulnerables.len(),227 "duplicate invulnerables in genesis."228 );229230 let bounded_invulnerables =231 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())232 .expect("genesis invulnerables are more than T::MaxCollators");233234 <Invulnerables<T>>::put(bounded_invulnerables);235 }236 }237238 #[pallet::event]239 #[pallet::generate_deposit(pub(super) fn deposit_event)]240 pub enum Event<T: Config> {241 InvulnerableAdded {242 invulnerable: T::AccountId,243 },244 InvulnerableRemoved {245 invulnerable: T::AccountId,246 },247 LicenseObtained {248 account_id: T::AccountId,249 deposit: BalanceOf<T>,250 },251 LicenseReleased {252 account_id: T::AccountId,253 deposit_returned: BalanceOf<T>,254 },255 CandidateAdded {256 account_id: T::AccountId,257 },258 CandidateRemoved {259 account_id: T::AccountId,260 },261 }262263 264 #[pallet::error]265 pub enum Error<T> {266 267 TooManyCandidates,268 269 Unknown,270 271 Permission,272 273 AlreadyHoldingLicense,274 275 NoLicense,276 277 AlreadyCandidate,278 279 NotCandidate,280 281 TooManyInvulnerables,282 283 TooFewInvulnerables,284 285 AlreadyInvulnerable,286 287 NotInvulnerable,288 289 NoAssociatedValidatorId,290 291 ValidatorNotRegistered,292 }293294 #[pallet::hooks]295 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}296297 #[pallet::call]298 impl<T: Config> Pallet<T> {299 300 #[pallet::call_index(0)]301 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]302 pub fn add_invulnerable(303 origin: OriginFor<T>,304 new: T::AccountId,305 ) -> DispatchResultWithPostInfo {306 T::UpdateOrigin::ensure_origin(origin)?;307308 309 let validator_key = T::ValidatorIdOf::convert(new.clone())310 .ok_or(Error::<T>::NoAssociatedValidatorId)?;311 ensure!(312 T::ValidatorRegistration::is_registered(&validator_key),313 Error::<T>::ValidatorNotRegistered314 );315 if Self::invulnerables().contains(&new) {316 return Ok(().into());317 }318319 <Invulnerables<T>>::try_append(new.clone())320 .map_err(|_| Error::<T>::TooManyInvulnerables)?;321322 323 let _ = Self::try_remove_candidate(&new);324325 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });326 Ok(().into())327 }328329 330 #[pallet::call_index(1)]331 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]332 pub fn remove_invulnerable(333 origin: OriginFor<T>,334 who: T::AccountId,335 ) -> DispatchResultWithPostInfo {336 T::UpdateOrigin::ensure_origin(origin)?;337338 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {339 if invulnerables.len() <= 1 {340 return Err(Error::<T>::TooFewInvulnerables.into());341 }342343 let index = invulnerables344 .into_iter()345 .position(|r| *r == who)346 .ok_or(Error::<T>::NotInvulnerable)?;347 invulnerables.remove(index);348 Ok(())349 })?;350 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });351 Ok(().into())352 }353354 355 356 357 358 359 #[pallet::call_index(2)]360 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]361 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {362 363 let who = ensure_signed(origin)?;364365 if LicenseDepositOf::<T>::contains_key(&who) {366 return Err(Error::<T>::AlreadyHoldingLicense.into());367 }368369 let validator_key = T::ValidatorIdOf::convert(who.clone())370 .ok_or(Error::<T>::NoAssociatedValidatorId)?;371 ensure!(372 T::ValidatorRegistration::is_registered(&validator_key),373 Error::<T>::ValidatorNotRegistered374 );375376 let deposit = T::LicenseBond::get();377378 T::Currency::hold(&T::LicenceBondIdentifier::get(), &who, deposit)?;379 LicenseDepositOf::<T>::insert(who.clone(), deposit);380381 Self::deposit_event(Event::LicenseObtained {382 account_id: who,383 deposit,384 });385 Ok(().into()) 386 }387388 389 390 391 392 #[pallet::call_index(3)]393 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]394 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {395 396 let who = ensure_signed(origin)?;397398 399 ensure!(400 LicenseDepositOf::<T>::contains_key(&who),401 Error::<T>::NoLicense402 );403 404 let length = <Candidates<T>>::decode_len().unwrap_or_default()405 + <Invulnerables<T>>::decode_len().unwrap_or_default();406 ensure!(407 (length as u32) < T::DesiredCollators::get(),408 Error::<T>::TooManyCandidates409 );410 ensure!(411 !Self::invulnerables().contains(&who),412 Error::<T>::AlreadyInvulnerable413 );414415 let current_count =416 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {417 if candidates.iter().any(|candidate| *candidate == who) {418 Err(Error::<T>::AlreadyCandidate)?419 } else {420 candidates421 .try_push(who.clone())422 .map_err(|_| Error::<T>::TooManyCandidates)?;423 424 <LastAuthoredBlock<T>>::insert(425 who.clone(),426 frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),427 );428 Ok(candidates.len())429 }430 })?;431432 Self::deposit_event(Event::CandidateAdded { account_id: who });433 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())434 }435436 437 438 #[pallet::call_index(4)]439 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]440 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {441 442 let who = ensure_signed(origin)?;443 let current_count = Self::try_remove_candidate(&who)?;444445 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())446 }447448 449 450 451 #[pallet::call_index(5)]452 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]453 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {454 455 let who = ensure_signed(origin)?;456457 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;458459 Ok(Some(<T as Config>::WeightInfo::release_license(460 current_count as u32,461 ))462 .into())463 }464465 466 467 468 469 470 #[pallet::call_index(6)]471 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]472 pub fn force_release_license(473 origin: OriginFor<T>,474 who: T::AccountId,475 ) -> DispatchResultWithPostInfo {476 477 T::UpdateOrigin::ensure_origin(origin)?;478479 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;480481 Ok(Some(<T as Config>::WeightInfo::force_release_license(482 current_count as u32,483 ))484 .into())485 }486 }487488 impl<T: Config> Pallet<T> {489 490 pub fn account_id() -> T::AccountId {491 T::PotId::get().into_account_truncating()492 }493494 495 496 fn try_remove_candidate_and_release_license(497 who: &T::AccountId,498 should_slash: bool,499 ignore_if_not_candidate: bool,500 ) -> Result<usize, DispatchError> {501 let current_count = Self::try_remove_candidate(who);502 let current_count = if ignore_if_not_candidate503 && current_count == Err(Error::<T>::NotCandidate.into())504 {505 <Candidates<T>>::decode_len().unwrap_or_default()506 } else {507 current_count?508 };509 Self::try_release_license(who, should_slash)?;510 Ok(current_count)511 }512513 514 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {515 let current_count =516 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {517 let index = candidates518 .iter()519 .position(|candidate| *candidate == *who)520 .ok_or(Error::<T>::NotCandidate)?;521 candidates.remove(index);522 <LastAuthoredBlock<T>>::remove(who.clone());523 Ok(candidates.len())524 })?;525 Self::deposit_event(Event::CandidateRemoved {526 account_id: who.clone(),527 });528 Ok(current_count)529 }530531 532 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {533 let mut deposit_returned = BalanceOf::<T>::default();534 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {535 if let Some(deposit) = deposit.take() {536 if should_slash {537 let slashed = T::SlashRatio::get() * deposit;538 let remaining = deposit - slashed;539540 let (imbalance, _) =541 T::Currency::slash(&T::LicenceBondIdentifier::get(), who, slashed);542 deposit_returned = remaining;543544 T::Currency::resolve(&T::TreasuryAccountId::get(), imbalance)545 .map_err(|_| DispatchError::Other("Failed to deposit imbalance"))?;546 } else {547 deposit_returned = deposit;548 }549550 T::Currency::release(551 &T::LicenceBondIdentifier::get(),552 who,553 deposit_returned,554 Precision::Exact,555 )?;556 Ok(())557 } else {558 Err(Error::<T>::NoLicense.into())559 }560 })?;561 Self::deposit_event(Event::LicenseReleased {562 account_id: who.clone(),563 deposit_returned,564 });565 Ok(())566 }567568 569 570 571 pub fn assemble_collators(572 candidates: BoundedVec<T::AccountId, T::MaxCollators>,573 ) -> Vec<T::AccountId> {574 let mut collators = Self::invulnerables().to_vec();575 collators.extend(candidates);576 collators577 }578579 580 581 pub fn kick_stale_candidates(582 candidates: BoundedVec<T::AccountId, T::MaxCollators>,583 ) -> BoundedVec<T::AccountId, T::MaxCollators> {584 let now = frame_system::Pallet::<T>::block_number();585 let kick_threshold = T::KickThreshold::get();586 candidates587 .into_iter()588 .filter_map(|c| {589 let last_block = <LastAuthoredBlock<T>>::get(c.clone());590 let since_last = now.saturating_sub(last_block);591 if since_last < kick_threshold {592 Some(c)593 } else {594 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);595 if let Err(why) = outcome {596 log::warn!("Failed to kick collator and release license {:?}", why);597 debug_assert!(false, "failed to kick collator and release license {why:?}");598 }599 None600 }601 })602 .collect::<Vec<_>>()603 .try_into()604 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")605 }606 }607608 609 610 impl<T: Config + pallet_authorship::Config>611 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>612 {613 fn note_author(author: T::AccountId) {614 let pot = Self::account_id();615 616 let reward = T::Currency::balance(&pot)617 .checked_sub(&T::Currency::minimum_balance())618 .unwrap_or_else(Zero::zero)619 .div(2u32.into());620621 if !reward.is_zero() {622 623 let _success = T::Currency::transfer(&pot, &author, reward, Preservation::Preserve);624 debug_assert!(_success.is_ok());625 }626 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());627628 frame_system::Pallet::<T>::register_extra_weight_unchecked(629 <T as Config>::WeightInfo::note_author(),630 DispatchClass::Mandatory,631 );632 }633 }634635 636 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {637 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {638 log::info!(639 "assembling new collators for new session {} at #{:?}",640 index,641 <frame_system::Pallet<T>>::block_number(),642 );643644 let candidates = Self::candidates();645 let candidates_len_before = candidates.len();646 let active_candidates = Self::kick_stale_candidates(candidates);647 let removed = candidates_len_before - active_candidates.len();648 let result = Self::assemble_collators(active_candidates);649650 frame_system::Pallet::<T>::register_extra_weight_unchecked(651 <T as Config>::WeightInfo::new_session(652 candidates_len_before as u32,653 removed as u32,654 ),655 DispatchClass::Mandatory,656 );657 Some(result)658 }659 fn start_session(_: SessionIndex) {660 661 }662 fn end_session(_: SessionIndex) {663 664 }665 }666}