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::*, Config as SystemConfig};109 use pallet_session::SessionManager;110 use sp_runtime::{111 Perbill,112 traits::{One, Convert},113 };114 use sp_staking::SessionIndex;115116 type BalanceOf<T> =117 <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;118119 120 121 pub struct IdentityCollator;122 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {123 fn convert(t: T) -> Option<T> {124 Some(t)125 }126 }127128 129 #[pallet::config]130 pub trait Config: frame_system::Config {131 132 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;133134 135 type Currency: ReservableCurrency<Self::AccountId>;136137 138 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;139140 141 type TreasuryAccountId: Get<Self::AccountId>;142143 144 type PotId: Get<PalletId>;145146 147 type MaxCollators: Get<u32>;148149 150 type SlashRatio: Get<Perbill>;151152 153 type ValidatorId: Member + Parameter;154155 156 157 158 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;159160 161 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;162163 164 type WeightInfo: WeightInfo;165 }166167 #[pallet::pallet]168 #[pallet::generate_store(pub(super) trait Store)]169 pub struct Pallet<T>(_);170171 172 #[pallet::storage]173 #[pallet::getter(fn invulnerables)]174 pub type Invulnerables<T: Config> =175 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;176177 178 #[pallet::storage]179 #[pallet::getter(fn licenses)]180 pub type Licenses<T: Config> =181 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;182183 184 #[pallet::storage]185 #[pallet::getter(fn candidates)]186 pub type Candidates<T: Config> =187 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;188189 190 191 192 #[pallet::storage]193 #[pallet::getter(fn kick_threshold)]194 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;195196 197 #[pallet::storage]198 #[pallet::getter(fn last_authored_block)]199 pub type LastAuthoredBlock<T: Config> =200 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;201202 203 204 205 #[pallet::storage]206 #[pallet::getter(fn desired_collators)]207 pub type DesiredCollators<T> = StorageValue<_, u32, ValueQuery>;208209 210 211 212 #[pallet::storage]213 #[pallet::getter(fn license_bond)]214 pub type LicenseBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;215216 #[pallet::genesis_config]217 pub struct GenesisConfig<T: Config> {218 pub invulnerables: Vec<T::AccountId>,219 pub license_bond: BalanceOf<T>,220 pub kick_threshold: T::BlockNumber,221 pub desired_collators: u32,222 }223224 #[cfg(feature = "std")]225 impl<T: Config> Default for GenesisConfig<T> {226 fn default() -> Self {227 Self {228 invulnerables: Default::default(),229 license_bond: Default::default(),230 kick_threshold: T::BlockNumber::one(),231 desired_collators: Default::default(),232 }233 }234 }235236 #[pallet::genesis_build]237 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {238 fn build(&self) {239 let duplicate_invulnerables = self240 .invulnerables241 .iter()242 .collect::<std::collections::BTreeSet<_>>();243 assert!(244 duplicate_invulnerables.len() == self.invulnerables.len(),245 "duplicate invulnerables in genesis."246 );247248 let bounded_invulnerables =249 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())250 .expect("genesis invulnerables are more than T::MaxCollators");251 assert!(252 T::MaxCollators::get() >= self.desired_collators,253 "genesis desired_collators are more than T::MaxCollators",254 );255256 <DesiredCollators<T>>::put(self.desired_collators);257 <LicenseBond<T>>::put(self.license_bond);258 <KickThreshold<T>>::put(self.kick_threshold);259 <Invulnerables<T>>::put(bounded_invulnerables);260 }261 }262263 #[pallet::event]264 #[pallet::generate_deposit(pub(super) fn deposit_event)]265 pub enum Event<T: Config> {266 NewDesiredCollators {267 desired_collators: u32,268 },269 NewLicenseBond {270 bond_amount: BalanceOf<T>,271 },272 NewKickThreshold {273 length_in_blocks: T::BlockNumber,274 },275 InvulnerableAdded {276 invulnerable: T::AccountId,277 },278 InvulnerableRemoved {279 invulnerable: T::AccountId,280 },281 LicenseObtained {282 account_id: T::AccountId,283 deposit: BalanceOf<T>,284 },285 LicenseForfeited {286 account_id: T::AccountId,287 deposit_returned: BalanceOf<T>,288 },289 CandidateAdded {290 account_id: T::AccountId,291 },292 CandidateRemoved {293 account_id: T::AccountId,294 },295 }296297 298 #[pallet::error]299 pub enum Error<T> {300 301 TooManyCandidates,302 303 Unknown,304 305 Permission,306 307 AlreadyHoldingLicense,308 309 NoLicense,310 311 AlreadyCandidate,312 313 NotCandidate,314 315 TooManyInvulnerables,316 317 TooFewInvulnerables,318 319 AlreadyInvulnerable,320 321 NotInvulnerable,322 323 NoAssociatedValidatorId,324 325 ValidatorNotRegistered,326 }327328 #[pallet::hooks]329 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}330331 #[pallet::call]332 impl<T: Config> Pallet<T> {333 334 #[pallet::weight(T::WeightInfo::set_invulnerables(1u32))] 335 pub fn add_invulnerable(336 origin: OriginFor<T>,337 new: T::AccountId,338 ) -> DispatchResultWithPostInfo {339 T::UpdateOrigin::ensure_origin(origin)?;340341 342 let validator_key = T::ValidatorIdOf::convert(new.clone())343 .ok_or(Error::<T>::NoAssociatedValidatorId)?;344 ensure!(345 T::ValidatorRegistration::is_registered(&validator_key),346 Error::<T>::ValidatorNotRegistered347 );348 if Self::invulnerables().contains(&new) {349 return Ok(().into());350 }351352 <Invulnerables<T>>::try_append(new.clone())353 .map_err(|_| Error::<T>::TooManyInvulnerables)?;354355 356 let _ = Self::try_remove_candidate(&new);357358 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });359 Ok(().into())360 }361362 363 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] 364 pub fn remove_invulnerable(365 origin: OriginFor<T>,366 who: T::AccountId,367 ) -> DispatchResultWithPostInfo {368 T::UpdateOrigin::ensure_origin(origin)?;369370 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {371 if invulnerables.len() <= 1 {372 return Err(Error::<T>::TooFewInvulnerables.into());373 }374375 let index = invulnerables376 .into_iter()377 .position(|r| *r == who)378 .ok_or(Error::<T>::NotInvulnerable)?;379 invulnerables.remove(index);380 Ok(())381 })?;382 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });383 Ok(().into())384 }385386 387 388 389 #[pallet::weight(T::WeightInfo::set_desired_collators())]390 pub fn set_desired_collators(origin: OriginFor<T>, max: u32) -> DispatchResultWithPostInfo {391 T::UpdateOrigin::ensure_origin(origin)?;392 393 if max > T::MaxCollators::get() {394 log::warn!("max > T::MaxCollators; you might need to run benchmarks again");395 }396 <DesiredCollators<T>>::put(max);397 Self::deposit_event(Event::NewDesiredCollators {398 desired_collators: max,399 });400 Ok(().into())401 }402403 404 #[pallet::weight(T::WeightInfo::set_license_bond())]405 pub fn set_license_bond(406 origin: OriginFor<T>,407 bond: BalanceOf<T>,408 ) -> DispatchResultWithPostInfo {409 T::UpdateOrigin::ensure_origin(origin)?;410 <LicenseBond<T>>::put(bond);411 Self::deposit_event(Event::NewLicenseBond { bond_amount: bond });412 Ok(().into())413 }414415 416 417 #[pallet::weight(T::WeightInfo::set_license_bond())] 418 pub fn set_kick_threshold(419 origin: OriginFor<T>,420 kick_threshold: T::BlockNumber,421 ) -> DispatchResultWithPostInfo {422 T::UpdateOrigin::ensure_origin(origin)?;423 424 <KickThreshold<T>>::put(kick_threshold);425 Self::deposit_event(Event::NewKickThreshold {426 length_in_blocks: kick_threshold,427 });428 Ok(().into())429 }430431 432 433 434 435 436 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] 437 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {438 439 let who = ensure_signed(origin)?;440441 if Licenses::<T>::contains_key(&who) {442 return Err(Error::<T>::AlreadyHoldingLicense.into());443 }444445 let validator_key = T::ValidatorIdOf::convert(who.clone())446 .ok_or(Error::<T>::NoAssociatedValidatorId)?;447 ensure!(448 T::ValidatorRegistration::is_registered(&validator_key),449 Error::<T>::ValidatorNotRegistered450 );451452 let deposit = Self::license_bond();453454 T::Currency::reserve(&who, deposit)?;455 Licenses::<T>::insert(who.clone(), deposit);456457 Self::deposit_event(Event::LicenseObtained {458 account_id: who,459 deposit,460 });461 Ok(().into()) 462 }463464 465 466 467 468 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] 469 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {470 471 let who = ensure_signed(origin)?;472473 474 ensure!(Licenses::<T>::contains_key(&who), Error::<T>::NoLicense);475 476 let length = <Candidates<T>>::decode_len().unwrap_or_default()477 + <Invulnerables<T>>::decode_len().unwrap_or_default();478 ensure!(479 (length as u32) < Self::desired_collators(),480 Error::<T>::TooManyCandidates481 );482 ensure!(483 !Self::invulnerables().contains(&who),484 Error::<T>::AlreadyInvulnerable485 );486487 let current_count =488 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {489 if candidates.iter().any(|candidate| *candidate == who) {490 Err(Error::<T>::AlreadyCandidate)?491 } else {492 candidates493 .try_push(who.clone())494 .map_err(|_| Error::<T>::TooManyCandidates)?;495 496 <LastAuthoredBlock<T>>::insert(497 who.clone(),498 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),499 );500 Ok(candidates.len())501 }502 })?;503504 Self::deposit_event(Event::CandidateAdded { account_id: who });505 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())506 }507508 509 510 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 511 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {512 513 let who = ensure_signed(origin)?;514 let current_count = Self::try_remove_candidate(&who)?;515516 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 517 }518519 520 521 522 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 523 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {524 525 let who = ensure_signed(origin)?;526527 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;528529 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 530 }531532 533 534 535 536 537 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] 538 pub fn force_revoke_license(539 origin: OriginFor<T>,540 who: T::AccountId,541 ) -> DispatchResultWithPostInfo {542 543 T::UpdateOrigin::ensure_origin(origin)?;544545 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;546547 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) 548 }549 }550551 impl<T: Config> Pallet<T> {552 553 pub fn account_id() -> T::AccountId {554 T::PotId::get().into_account_truncating()555 }556557 558 559 fn try_remove_candidate_and_release_license(560 who: &T::AccountId,561 should_slash: bool,562 ignore_if_not_candidate: bool,563 ) -> Result<usize, DispatchError> {564 let current_count = Self::try_remove_candidate(who);565 let current_count = if ignore_if_not_candidate566 && current_count == Err(Error::<T>::NotCandidate.into())567 {568 <Candidates<T>>::decode_len().unwrap_or_default()569 } else {570 current_count?571 };572 Self::try_release_license(who, should_slash)?;573 Ok(current_count)574 }575576 577 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {578 let current_count =579 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {580 let index = candidates581 .iter()582 .position(|candidate| *candidate == *who)583 .ok_or(Error::<T>::NotCandidate)?;584 candidates.remove(index);585 <LastAuthoredBlock<T>>::remove(who.clone());586 Ok(candidates.len())587 })?;588 Self::deposit_event(Event::CandidateRemoved {589 account_id: who.clone(),590 });591 Ok(current_count)592 }593594 595 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {596 let mut deposit_returned = BalanceOf::<T>::default();597 Licenses::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {598 if let Some(deposit) = deposit.take() {599 if should_slash {600 let slashed = T::SlashRatio::get() * deposit;601 let remaining = deposit - slashed;602603 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);604 605 deposit_returned = remaining;606607 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);608 } else {609 610 deposit_returned = deposit;611 }612613 T::Currency::unreserve(who, deposit_returned);614 Ok(())615 } else {616 Err(Error::<T>::NoLicense.into())617 }618 })?;619 Self::deposit_event(Event::LicenseForfeited {620 account_id: who.clone(),621 deposit_returned,622 });623 Ok(())624 }625626 627 628 629 pub fn assemble_collators(630 candidates: BoundedVec<T::AccountId, T::MaxCollators>,631 ) -> Vec<T::AccountId> {632 let mut collators = Self::invulnerables().to_vec();633 collators.extend(candidates);634 collators635 }636637 638 639 pub fn kick_stale_candidates(640 candidates: BoundedVec<T::AccountId, T::MaxCollators>,641 ) -> BoundedVec<T::AccountId, T::MaxCollators> {642 let now = frame_system::Pallet::<T>::block_number();643 let kick_threshold = Self::kick_threshold();644 candidates645 .into_iter()646 .filter_map(|c| {647 let last_block = <LastAuthoredBlock<T>>::get(c.clone());648 let since_last = now.saturating_sub(last_block);649 if since_last < kick_threshold {650 Some(c)651 } else {652 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);653 if let Err(why) = outcome {654 log::warn!("Failed to kick collator and release license {:?}", why);655 debug_assert!(false, "failed to kick collator and release license {why:?}");656 }657 None658 }659 })660 .collect::<Vec<_>>()661 .try_into()662 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")663 }664 }665666 667 668 impl<T: Config + pallet_authorship::Config>669 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>670 {671 fn note_author(author: T::AccountId) {672 let pot = Self::account_id();673 674 let reward = T::Currency::free_balance(&pot)675 .checked_sub(&T::Currency::minimum_balance())676 .unwrap_or_else(Zero::zero)677 .div(2u32.into());678 679 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);680 debug_assert!(_success.is_ok());681 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());682683 frame_system::Pallet::<T>::register_extra_weight_unchecked(684 T::WeightInfo::note_author(),685 DispatchClass::Mandatory,686 );687 }688689 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {690 691 }692 }693694 695 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {696 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {697 log::info!(698 "assembling new collators for new session {} at #{:?}",699 index,700 <frame_system::Pallet<T>>::block_number(),701 );702703 let candidates = Self::candidates();704 let candidates_len_before = candidates.len();705 let active_candidates = Self::kick_stale_candidates(candidates);706 let removed = candidates_len_before - active_candidates.len();707 let result = Self::assemble_collators(active_candidates);708709 frame_system::Pallet::<T>::register_extra_weight_unchecked(710 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),711 DispatchClass::Mandatory,712 );713 Some(result)714 }715 fn start_session(_: SessionIndex) {716 717 }718 fn end_session(_: SessionIndex) {719 720 }721 }722}