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::{102 traits::{AccountIdConversion, CheckedSub, Saturating, Zero},103 RuntimeDebug,104 },105 traits::{106 Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,107 ValidatorRegistration,108 },109 BoundedVec, PalletId,110 };111 use frame_system::{pallet_prelude::*, Config as SystemConfig};112 use pallet_session::SessionManager;113 use sp_runtime::{114 Perbill,115 traits::{One, Convert},116 };117 use sp_staking::SessionIndex;118119 type BalanceOf<T> =120 <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;121122 123 124 pub struct IdentityCollator;125 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {126 fn convert(t: T) -> Option<T> {127 Some(t)128 }129 }130131 132 #[pallet::config]133 pub trait Config: frame_system::Config {134 135 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;136137 138 type Currency: ReservableCurrency<Self::AccountId>;139140 141 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;142143 144 type TreasuryAccountId: Get<Self::AccountId>;145146 147 type PotId: Get<PalletId>;148149 150 151 152 type MaxCandidates: Get<u32>;153154 155 156 157 type MinCandidates: Get<u32>;158159 160 type MaxInvulnerables: Get<u32>;161162 163 type SlashRatio: Get<Perbill>;164165 166 type ValidatorId: Member + Parameter;167168 169 170 171 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;172173 174 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;175176 177 type WeightInfo: WeightInfo;178 }179180 181 #[derive(182 PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,183 )]184 pub struct CandidateInfo<AccountId, Balance> {185 186 pub who: AccountId,187 188 pub deposit: Balance,189 }190191 #[pallet::pallet]192 #[pallet::generate_store(pub(super) trait Store)]193 pub struct Pallet<T>(_);194195 196 #[pallet::storage]197 #[pallet::getter(fn invulnerables)]198 pub type Invulnerables<T: Config> =199 StorageValue<_, BoundedVec<T::AccountId, T::MaxInvulnerables>, ValueQuery>;200201 202 #[pallet::storage]203 #[pallet::getter(fn candidates)]204 pub type Candidates<T: Config> = StorageValue<205 _,206 BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,207 ValueQuery,208 >;209210 211 212 213 #[pallet::storage]214 #[pallet::getter(fn kick_threshold)]215 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;216217 218 #[pallet::storage]219 #[pallet::getter(fn last_authored_block)]220 pub type LastAuthoredBlock<T: Config> =221 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;222223 224 225 226 #[pallet::storage]227 #[pallet::getter(fn desired_candidates)]228 pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;229230 231 232 233 #[pallet::storage]234 #[pallet::getter(fn candidacy_bond)]235 pub type CandidacyBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;236237 #[pallet::genesis_config]238 pub struct GenesisConfig<T: Config> {239 pub invulnerables: Vec<T::AccountId>,240 pub candidacy_bond: BalanceOf<T>,241 pub kick_threshold: T::BlockNumber,242 pub desired_candidates: u32,243 }244245 #[cfg(feature = "std")]246 impl<T: Config> Default for GenesisConfig<T> {247 fn default() -> Self {248 Self {249 invulnerables: Default::default(),250 candidacy_bond: Default::default(),251 kick_threshold: T::BlockNumber::one(),252 desired_candidates: Default::default(),253 }254 }255 }256257 #[pallet::genesis_build]258 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {259 fn build(&self) {260 let duplicate_invulnerables = self261 .invulnerables262 .iter()263 .collect::<std::collections::BTreeSet<_>>();264 assert!(265 duplicate_invulnerables.len() == self.invulnerables.len(),266 "duplicate invulnerables in genesis."267 );268269 let bounded_invulnerables =270 BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())271 .expect("genesis invulnerables are more than T::MaxInvulnerables");272 assert!(273 T::MaxCandidates::get() >= self.desired_candidates,274 "genesis desired_candidates are more than T::MaxCandidates",275 );276277 <DesiredCandidates<T>>::put(&self.desired_candidates);278 <CandidacyBond<T>>::put(&self.candidacy_bond);279 <KickThreshold<T>>::put(&self.kick_threshold);280 <Invulnerables<T>>::put(bounded_invulnerables);281 }282 }283284 #[pallet::event]285 #[pallet::generate_deposit(pub(super) fn deposit_event)]286 pub enum Event<T: Config> {287 NewDesiredCandidates {288 desired_candidates: u32,289 },290 NewCandidacyBond {291 bond_amount: BalanceOf<T>,292 },293 NewKickThreshold {294 length_in_blocks: T::BlockNumber,295 },296 InvulnerableAdded {297 invulnerable: T::AccountId,298 },299 InvulnerableRemoved {300 invulnerable: T::AccountId,301 },302 CandidateAdded {303 account_id: T::AccountId,304 deposit: BalanceOf<T>,305 },306 CandidateRemoved {307 account_id: T::AccountId,308 deposit_returned: BalanceOf<T>,309 },310 }311312 313 #[pallet::error]314 pub enum Error<T> {315 316 TooManyCandidates,317 318 TooFewCandidates,319 320 Unknown,321 322 Permission,323 324 AlreadyCandidate,325 326 NotCandidate,327 328 TooManyInvulnerables,329 330 TooFewInvulnerables,331 332 AlreadyInvulnerable,333 334 NotInvulnerable,335 336 NoAssociatedValidatorId,337 338 ValidatorNotRegistered,339 }340341 #[pallet::hooks]342 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}343344 #[pallet::call]345 impl<T: Config> Pallet<T> {346 347 #[pallet::weight(T::WeightInfo::set_invulnerables(1 as u32))] 348 pub fn add_invulnerable(349 origin: OriginFor<T>,350 new: T::AccountId,351 ) -> DispatchResultWithPostInfo {352 T::UpdateOrigin::ensure_origin(origin)?;353354 355 let validator_key = T::ValidatorIdOf::convert(new.clone())356 .ok_or(Error::<T>::NoAssociatedValidatorId)?;357 ensure!(358 T::ValidatorRegistration::is_registered(&validator_key),359 Error::<T>::ValidatorNotRegistered360 );361 362 if Self::invulnerables().contains(&new) {363 return Ok(().into());364 }365366 <Invulnerables<T>>::try_append(new.clone())367 .map_err(|_| Error::<T>::TooManyInvulnerables)?;368 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });369 Ok(().into())370 }371372 373 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] 374 pub fn remove_invulnerable(375 origin: OriginFor<T>,376 who: T::AccountId,377 ) -> DispatchResultWithPostInfo {378 T::UpdateOrigin::ensure_origin(origin)?;379380 381 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {382 if invulnerables.len() <= 1 {383 return Err(Error::<T>::TooFewInvulnerables.into());384 }385386 let index = invulnerables387 .into_iter()388 .position(|r| *r == who)389 .ok_or(Error::<T>::NotInvulnerable)?;390 invulnerables.remove(index);391 Ok(())392 })?;393 394395396397 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });398 Ok(().into())399 }400401 402 403 404 #[pallet::weight(T::WeightInfo::set_desired_candidates())]405 pub fn set_desired_candidates(406 origin: OriginFor<T>,407 max: u32,408 ) -> DispatchResultWithPostInfo {409 T::UpdateOrigin::ensure_origin(origin)?;410 411 if max > T::MaxCandidates::get() {412 log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");413 }414 <DesiredCandidates<T>>::put(&max);415 Self::deposit_event(Event::NewDesiredCandidates {416 desired_candidates: max,417 });418 Ok(().into())419 }420421 422 #[pallet::weight(T::WeightInfo::set_candidacy_bond())]423 pub fn set_candidacy_bond(424 origin: OriginFor<T>,425 bond: BalanceOf<T>,426 ) -> DispatchResultWithPostInfo {427 T::UpdateOrigin::ensure_origin(origin)?;428 <CandidacyBond<T>>::put(&bond);429 Self::deposit_event(Event::NewCandidacyBond { bond_amount: bond });430 Ok(().into())431 }432433 434 435 #[pallet::weight(T::WeightInfo::set_candidacy_bond())] 436 pub fn set_kick_threshold(437 origin: OriginFor<T>,438 kick_threshold: T::BlockNumber,439 ) -> DispatchResultWithPostInfo {440 T::UpdateOrigin::ensure_origin(origin)?;441 442 <KickThreshold<T>>::put(kick_threshold);443 Self::deposit_event(Event::NewKickThreshold {444 length_in_blocks: kick_threshold,445 });446 Ok(().into())447 }448449 450 451 452 453 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))]454 pub fn register_as_candidate(origin: OriginFor<T>) -> DispatchResultWithPostInfo {455 let who = ensure_signed(origin)?;456457 458 let length = <Candidates<T>>::decode_len().unwrap_or_default();459 ensure!(460 (length as u32) < Self::desired_candidates(),461 Error::<T>::TooManyCandidates462 );463 464 ensure!(465 !Self::invulnerables().contains(&who),466 Error::<T>::AlreadyInvulnerable467 );468469 let validator_key = T::ValidatorIdOf::convert(who.clone())470 .ok_or(Error::<T>::NoAssociatedValidatorId)?;471 ensure!(472 T::ValidatorRegistration::is_registered(&validator_key),473 Error::<T>::ValidatorNotRegistered474 );475476 let deposit = Self::candidacy_bond();477 478 let incoming = CandidateInfo {479 who: who.clone(),480 deposit,481 };482483 let current_count =484 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {485 if candidates.iter().any(|candidate| candidate.who == who) {486 Err(Error::<T>::AlreadyCandidate)?487 } else {488 T::Currency::reserve(&who, deposit)?;489 candidates490 .try_push(incoming)491 .map_err(|_| Error::<T>::TooManyCandidates)?;492 <LastAuthoredBlock<T>>::insert(493 who.clone(),494 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),495 );496 Ok(candidates.len())497 }498 })?;499500 Self::deposit_event(Event::CandidateAdded {501 account_id: who,502 deposit,503 });504 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())505 }506507 508 509 510 511 512 513 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]514 pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {515 let who = ensure_signed(origin)?;516 517 ensure!(518 Self::candidates().len() as u32 > T::MinCandidates::get(),519 Error::<T>::TooFewCandidates520 );521 let current_count = Self::try_remove_candidate(&who, false)?;522523 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())524 }525 }526527 impl<T: Config> Pallet<T> {528 529 pub fn account_id() -> T::AccountId {530 T::PotId::get().into_account_truncating()531 }532533 534 fn try_remove_candidate(535 who: &T::AccountId,536 should_slash: bool,537 ) -> Result<usize, DispatchError> {538 let mut deposit_returned = BalanceOf::<T>::default();539 let current_count =540 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {541 let index = candidates542 .iter()543 .position(|candidate| candidate.who == *who)544 .ok_or(Error::<T>::NotCandidate)?;545 let candidate = candidates.remove(index);546 let deposit = candidate.deposit;547548 if should_slash {549 let slashed = T::SlashRatio::get() * deposit;550 let remaining = deposit - slashed;551552 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);553 554 deposit_returned = remaining;555556 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);557558 559 } else {560 561 deposit_returned = deposit;562 }563564 T::Currency::unreserve(who, deposit_returned);565 566 <LastAuthoredBlock<T>>::remove(who.clone());567 Ok(candidates.len())568 })?;569 Self::deposit_event(Event::CandidateRemoved {570 account_id: who.clone(),571 deposit_returned,572 });573 Ok(current_count)574 }575576 577 578 579 pub fn assemble_collators(580 candidates: BoundedVec<T::AccountId, T::MaxCandidates>,581 ) -> Vec<T::AccountId> {582 let mut collators = Self::invulnerables().to_vec();583 collators.extend(candidates);584 collators585 }586587 588 589 pub fn kick_stale_candidates(590 candidates: BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,591 ) -> BoundedVec<T::AccountId, T::MaxCandidates> {592 let now = frame_system::Pallet::<T>::block_number();593 let kick_threshold = Self::kick_threshold();594 candidates595 .into_iter()596 .filter_map(|c| {597 let last_block = <LastAuthoredBlock<T>>::get(c.who.clone());598 let since_last = now.saturating_sub(last_block);599 if since_last < kick_threshold ||600 Self::candidates().len() as u32 <= T::MinCandidates::get()601 {602 Some(c.who)603 } else {604 let outcome = Self::try_remove_candidate(&c.who, true);605 if let Err(why) = outcome {606 log::warn!("Failed to remove candidate {:?}", why);607 debug_assert!(false, "failed to remove candidate {:?}", why);608 }609 None610 }611 })612 .collect::<Vec<_>>()613 .try_into()614 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")615 }616 }617618 619 620 impl<T: Config + pallet_authorship::Config>621 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>622 {623 fn note_author(author: T::AccountId) {624 let pot = Self::account_id();625 626 let reward = T::Currency::free_balance(&pot)627 .checked_sub(&T::Currency::minimum_balance())628 .unwrap_or_else(Zero::zero)629 .div(2u32.into());630 631 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);632 debug_assert!(_success.is_ok());633 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());634635 frame_system::Pallet::<T>::register_extra_weight_unchecked(636 T::WeightInfo::note_author(),637 DispatchClass::Mandatory,638 );639 }640641 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {642 643 }644 }645646 647 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {648 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {649 log::info!(650 "assembling new collators for new session {} at #{:?}",651 index,652 <frame_system::Pallet<T>>::block_number(),653 );654655 let candidates = Self::candidates();656 let candidates_len_before = candidates.len();657 let active_candidates = Self::kick_stale_candidates(candidates);658 let removed = candidates_len_before - active_candidates.len();659 let result = Self::assemble_collators(active_candidates);660661 frame_system::Pallet::<T>::register_extra_weight_unchecked(662 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),663 DispatchClass::Mandatory,664 );665 Some(result)666 }667 fn start_session(_: SessionIndex) {668 669 }670 fn end_session(_: SessionIndex) {671 672 }673 }674}