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 core::ops::Div;100101 use frame_support::{102 dispatch::{DispatchClass, DispatchResultWithPostInfo},103 pallet_prelude::*,104 sp_runtime::traits::{AccountIdConversion, CheckedSub, Saturating, Zero},105 traits::{106 fungible::{Balanced, BalancedHold, Inspect, Mutate, MutateHold},107 tokens::{Precision, Preservation},108 EnsureOrigin, ValidatorRegistration,109 },110 BoundedVec, PalletId,111 };112 use frame_system::pallet_prelude::*;113 use pallet_session::SessionManager;114 use sp_runtime::{traits::Convert, Perbill};115 use sp_staking::SessionIndex;116 use sp_std::vec::Vec;117118 use super::*;119 pub use crate::weights::WeightInfo;120121 122 123 pub struct IdentityCollator;124 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {125 fn convert(t: T) -> Option<T> {126 Some(t)127 }128 }129130 131 #[pallet::config]132 pub trait Config: frame_system::Config {133 134 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;135 136 type RuntimeHoldReason: From<HoldReason>;137138 type Currency: Mutate<Self::AccountId>139 + MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>140 + BalancedHold<Self::AccountId>;141142 143 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;144145 146 type TreasuryAccountId: Get<Self::AccountId>;147148 149 type PotId: Get<PalletId>;150151 152 type MaxCollators: Get<u32>;153154 155 type SlashRatio: Get<Perbill>;156157 158 type ValidatorId: Member + Parameter;159160 161 162 163 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;164165 166 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;167168 169 type WeightInfo: WeightInfo;170171 type DesiredCollators: Get<u32>;172173 type LicenseBond: Get<BalanceOf<Self>>;174175 type KickThreshold: Get<BlockNumberFor<Self>>;176 }177178 #[pallet::composite_enum]179 pub enum HoldReason {180 181 LicenseBond,182 }183184 #[pallet::pallet]185 pub struct Pallet<T>(_);186187 188 #[pallet::storage]189 #[pallet::getter(fn invulnerables)]190 pub type Invulnerables<T: Config> =191 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;192193 194 #[pallet::storage]195 #[pallet::getter(fn license_deposit_of)]196 pub type LicenseDepositOf<T: Config> =197 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;198199 200 #[pallet::storage]201 #[pallet::getter(fn candidates)]202 pub type Candidates<T: Config> =203 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;204205 206 #[pallet::storage]207 #[pallet::getter(fn last_authored_block)]208 pub type LastAuthoredBlock<T: Config> =209 StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor<T>, ValueQuery>;210211 #[pallet::genesis_config]212 pub struct GenesisConfig<T: Config> {213 pub invulnerables: Vec<T::AccountId>,214 }215216 impl<T: Config> Default for GenesisConfig<T> {217 fn default() -> Self {218 Self {219 invulnerables: Default::default(),220 }221 }222 }223224 #[pallet::genesis_build]225 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {226 fn build(&self) {227 use sp_std::collections::btree_set::BTreeSet;228229 let duplicate_invulnerables = self.invulnerables.iter().collect::<BTreeSet<_>>();230 assert!(231 duplicate_invulnerables.len() == self.invulnerables.len(),232 "duplicate invulnerables in genesis."233 );234235 let bounded_invulnerables =236 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())237 .expect("genesis invulnerables are more than T::MaxCollators");238239 <Invulnerables<T>>::put(bounded_invulnerables);240 }241 }242243 #[pallet::event]244 #[pallet::generate_deposit(pub(super) fn deposit_event)]245 pub enum Event<T: Config> {246 InvulnerableAdded {247 invulnerable: T::AccountId,248 },249 InvulnerableRemoved {250 invulnerable: T::AccountId,251 },252 LicenseObtained {253 account_id: T::AccountId,254 deposit: BalanceOf<T>,255 },256 LicenseReleased {257 account_id: T::AccountId,258 deposit_returned: BalanceOf<T>,259 },260 CandidateAdded {261 account_id: T::AccountId,262 },263 CandidateRemoved {264 account_id: T::AccountId,265 },266 }267268 269 #[pallet::error]270 pub enum Error<T> {271 272 TooManyCandidates,273 274 Unknown,275 276 Permission,277 278 AlreadyHoldingLicense,279 280 NoLicense,281 282 AlreadyCandidate,283 284 NotCandidate,285 286 TooManyInvulnerables,287 288 TooFewInvulnerables,289 290 AlreadyInvulnerable,291 292 NotInvulnerable,293 294 NoAssociatedValidatorId,295 296 ValidatorNotRegistered,297 }298299 #[pallet::hooks]300 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}301302 #[pallet::call]303 impl<T: Config> Pallet<T> {304 305 #[pallet::call_index(0)]306 #[pallet::weight(<T as Config>::WeightInfo::add_invulnerable(T::MaxCollators::get()))]307 pub fn add_invulnerable(308 origin: OriginFor<T>,309 new: T::AccountId,310 ) -> DispatchResultWithPostInfo {311 T::UpdateOrigin::ensure_origin(origin)?;312313 314 let validator_key = T::ValidatorIdOf::convert(new.clone())315 .ok_or(Error::<T>::NoAssociatedValidatorId)?;316 ensure!(317 T::ValidatorRegistration::is_registered(&validator_key),318 Error::<T>::ValidatorNotRegistered319 );320 if Self::invulnerables().contains(&new) {321 return Ok(().into());322 }323324 <Invulnerables<T>>::try_append(new.clone())325 .map_err(|_| Error::<T>::TooManyInvulnerables)?;326327 328 let _ = Self::try_remove_candidate(&new);329330 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });331 Ok(().into())332 }333334 335 #[pallet::call_index(1)]336 #[pallet::weight(<T as Config>::WeightInfo::remove_invulnerable(T::MaxCollators::get()))]337 pub fn remove_invulnerable(338 origin: OriginFor<T>,339 who: T::AccountId,340 ) -> DispatchResultWithPostInfo {341 T::UpdateOrigin::ensure_origin(origin)?;342343 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {344 if invulnerables.len() <= 1 {345 return Err(Error::<T>::TooFewInvulnerables.into());346 }347348 let index = invulnerables349 .into_iter()350 .position(|r| *r == who)351 .ok_or(Error::<T>::NotInvulnerable)?;352 invulnerables.remove(index);353 Ok(())354 })?;355 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });356 Ok(().into())357 }358359 360 361 362 363 364 #[pallet::call_index(2)]365 #[pallet::weight(<T as Config>::WeightInfo::get_license(T::MaxCollators::get()))]366 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {367 368 let who = ensure_signed(origin)?;369370 if LicenseDepositOf::<T>::contains_key(&who) {371 return Err(Error::<T>::AlreadyHoldingLicense.into());372 }373374 let validator_key = T::ValidatorIdOf::convert(who.clone())375 .ok_or(Error::<T>::NoAssociatedValidatorId)?;376 ensure!(377 T::ValidatorRegistration::is_registered(&validator_key),378 Error::<T>::ValidatorNotRegistered379 );380381 let deposit = T::LicenseBond::get();382383 T::Currency::hold(&HoldReason::LicenseBond.into(), &who, deposit)?;384 LicenseDepositOf::<T>::insert(who.clone(), deposit);385386 Self::deposit_event(Event::LicenseObtained {387 account_id: who,388 deposit,389 });390 Ok(().into()) 391 }392393 394 395 396 397 #[pallet::call_index(3)]398 #[pallet::weight(<T as Config>::WeightInfo::onboard(T::MaxCollators::get()))]399 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {400 401 let who = ensure_signed(origin)?;402403 404 ensure!(405 LicenseDepositOf::<T>::contains_key(&who),406 Error::<T>::NoLicense407 );408 409 let length = <Candidates<T>>::decode_len().unwrap_or_default()410 + <Invulnerables<T>>::decode_len().unwrap_or_default();411 ensure!(412 (length as u32) < T::DesiredCollators::get(),413 Error::<T>::TooManyCandidates414 );415 ensure!(416 !Self::invulnerables().contains(&who),417 Error::<T>::AlreadyInvulnerable418 );419420 let current_count =421 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {422 if candidates.iter().any(|candidate| *candidate == who) {423 Err(Error::<T>::AlreadyCandidate)?424 } else {425 candidates426 .try_push(who.clone())427 .map_err(|_| Error::<T>::TooManyCandidates)?;428 429 <LastAuthoredBlock<T>>::insert(430 who.clone(),431 frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),432 );433 Ok(candidates.len())434 }435 })?;436437 Self::deposit_event(Event::CandidateAdded { account_id: who });438 Ok(Some(<T as Config>::WeightInfo::onboard(current_count as u32)).into())439 }440441 442 443 #[pallet::call_index(4)]444 #[pallet::weight(<T as Config>::WeightInfo::offboard(T::MaxCollators::get()))]445 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {446 447 let who = ensure_signed(origin)?;448 let current_count = Self::try_remove_candidate(&who)?;449450 Ok(Some(<T as Config>::WeightInfo::offboard(current_count as u32)).into())451 }452453 454 455 456 #[pallet::call_index(5)]457 #[pallet::weight(<T as Config>::WeightInfo::release_license(T::MaxCollators::get()))]458 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {459 460 let who = ensure_signed(origin)?;461462 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;463464 Ok(Some(<T as Config>::WeightInfo::release_license(465 current_count as u32,466 ))467 .into())468 }469470 471 472 473 474 475 #[pallet::call_index(6)]476 #[pallet::weight(<T as Config>::WeightInfo::force_release_license(T::MaxCollators::get()))]477 pub fn force_release_license(478 origin: OriginFor<T>,479 who: T::AccountId,480 ) -> DispatchResultWithPostInfo {481 482 T::UpdateOrigin::ensure_origin(origin)?;483484 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;485486 Ok(Some(<T as Config>::WeightInfo::force_release_license(487 current_count as u32,488 ))489 .into())490 }491 }492493 impl<T: Config> Pallet<T> {494 495 pub fn account_id() -> T::AccountId {496 T::PotId::get().into_account_truncating()497 }498499 500 501 fn try_remove_candidate_and_release_license(502 who: &T::AccountId,503 should_slash: bool,504 ignore_if_not_candidate: bool,505 ) -> Result<usize, DispatchError> {506 let current_count = Self::try_remove_candidate(who);507 let current_count = if ignore_if_not_candidate508 && current_count == Err(Error::<T>::NotCandidate.into())509 {510 <Candidates<T>>::decode_len().unwrap_or_default()511 } else {512 current_count?513 };514 Self::try_release_license(who, should_slash)?;515 Ok(current_count)516 }517518 519 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {520 let current_count =521 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {522 let index = candidates523 .iter()524 .position(|candidate| *candidate == *who)525 .ok_or(Error::<T>::NotCandidate)?;526 candidates.remove(index);527 <LastAuthoredBlock<T>>::remove(who.clone());528 Ok(candidates.len())529 })?;530 Self::deposit_event(Event::CandidateRemoved {531 account_id: who.clone(),532 });533 Ok(current_count)534 }535536 537 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {538 let mut deposit_returned = BalanceOf::<T>::default();539 LicenseDepositOf::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {540 if let Some(deposit) = deposit.take() {541 if should_slash {542 let slashed = T::SlashRatio::get() * deposit;543 let remaining = deposit - slashed;544545 let (imbalance, _) =546 T::Currency::slash(&HoldReason::LicenseBond.into(), who, slashed);547 deposit_returned = remaining;548549 T::Currency::resolve(&T::TreasuryAccountId::get(), imbalance)550 .map_err(|_| DispatchError::Other("Failed to deposit imbalance"))?;551 } else {552 deposit_returned = deposit;553 }554555 T::Currency::release(556 &HoldReason::LicenseBond.into(),557 who,558 deposit_returned,559 Precision::Exact,560 )?;561 Ok(())562 } else {563 Err(Error::<T>::NoLicense.into())564 }565 })?;566 Self::deposit_event(Event::LicenseReleased {567 account_id: who.clone(),568 deposit_returned,569 });570 Ok(())571 }572573 574 575 576 pub fn assemble_collators(577 candidates: BoundedVec<T::AccountId, T::MaxCollators>,578 ) -> Vec<T::AccountId> {579 let mut collators = Self::invulnerables().to_vec();580 collators.extend(candidates);581 collators582 }583584 585 586 pub fn kick_stale_candidates(587 candidates: BoundedVec<T::AccountId, T::MaxCollators>,588 ) -> BoundedVec<T::AccountId, T::MaxCollators> {589 let now = frame_system::Pallet::<T>::block_number();590 let kick_threshold = T::KickThreshold::get();591 candidates592 .into_iter()593 .filter_map(|c| {594 let last_block = <LastAuthoredBlock<T>>::get(c.clone());595 let since_last = now.saturating_sub(last_block);596 if since_last < kick_threshold {597 Some(c)598 } else {599 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);600 if let Err(why) = outcome {601 log::warn!("Failed to kick collator and release license {:?}", why);602 debug_assert!(false, "failed to kick collator and release license {why:?}");603 }604 None605 }606 })607 .collect::<Vec<_>>()608 .try_into()609 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")610 }611 }612613 614 615 impl<T: Config + pallet_authorship::Config>616 pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T>617 {618 fn note_author(author: T::AccountId) {619 let pot = Self::account_id();620 621 let reward = T::Currency::balance(&pot)622 .checked_sub(&T::Currency::minimum_balance())623 .unwrap_or_else(Zero::zero)624 .div(2u32.into());625626 if !reward.is_zero() {627 628 let _success = T::Currency::transfer(&pot, &author, reward, Preservation::Preserve);629 debug_assert!(_success.is_ok());630 }631 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());632633 frame_system::Pallet::<T>::register_extra_weight_unchecked(634 <T as Config>::WeightInfo::note_author(),635 DispatchClass::Mandatory,636 );637 }638 }639640 641 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {642 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {643 log::info!(644 "assembling new collators for new session {} at #{:?}",645 index,646 <frame_system::Pallet<T>>::block_number(),647 );648649 let candidates = Self::candidates();650 let candidates_len_before = candidates.len();651 let active_candidates = Self::kick_stale_candidates(candidates);652 let removed = candidates_len_before - active_candidates.len();653 let result = Self::assemble_collators(active_candidates);654655 frame_system::Pallet::<T>::register_extra_weight_unchecked(656 <T as Config>::WeightInfo::new_session(657 candidates_len_before as u32,658 removed as u32,659 ),660 DispatchClass::Mandatory,661 );662 Some(result)663 }664 fn start_session(_: SessionIndex) {665 666 }667 fn end_session(_: SessionIndex) {668 669 }670 }671}