difftreelog
feat(collator-selection) method refactoring + unit tests complete
in: master
9 files changed
pallets/collator-selection/src/benchmarking.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/benchmarking.rs
+++ b/pallets/collator-selection/src/benchmarking.rs
@@ -130,7 +130,7 @@
where_clause { where T: pallet_authorship::Config + session::Config }
set_invulnerables {
- let b in 1 .. T::MaxInvulnerables::get();
+ let b in 1 .. T::MaxCollators::get();
let new_invulnerables = register_validators::<T>(b);
let origin = T::UpdateOrigin::successful_origin();
}: {
@@ -142,16 +142,16 @@
assert_last_event::<T>(Event::NewInvulnerables{invulnerables: new_invulnerables}.into());
}
- set_desired_candidates {
+ set_desired_collators {
let max: u32 = 999;
let origin = T::UpdateOrigin::successful_origin();
}: {
assert_ok!(
- <CollatorSelection<T>>::set_desired_candidates(origin, max.clone())
+ <CollatorSelection<T>>::set_desired_collators(origin, max.clone())
);
}
verify {
- assert_last_event::<T>(Event::NewDesiredCandidates{desired_candidates: max}.into());
+ assert_last_event::<T>(Event::NewDesiredCollators{desired_collators: max}.into());
}
set_license_bond {
@@ -169,10 +169,10 @@
// worse case is when we have all the max-candidate slots filled except one, and we fill that
// one.
register_as_candidate {
- let c in 1 .. T::MaxCandidates::get();
+ let c in 1 .. T::MaxCollators::get();
<LicenseBond<T>>::put(T::Currency::minimum_balance());
- <DesiredCandidates<T>>::put(c + 1);
+ <DesiredCollators<T>>::put(c + 1);
register_validators::<T>(c);
register_candidates::<T>(c);
@@ -194,9 +194,9 @@
// worse case is the last candidate leaving.
leave_intent {
- let c in (T::MinCandidates::get() + 1) .. T::MaxCandidates::get();
+ let c in (T::MinCandidates::get() + 1) .. T::MaxCollators::get();
<LicenseBond<T>>::put(T::Currency::minimum_balance());
- <DesiredCandidates<T>>::put(c);
+ <DesiredCollators<T>>::put(c);
register_validators::<T>(c);
register_candidates::<T>(c);
@@ -230,11 +230,11 @@
// worst case for new session.
new_session {
- let r in 1 .. T::MaxCandidates::get();
- let c in 1 .. T::MaxCandidates::get();
+ let r in 1 .. T::MaxCollators::get();
+ let c in 1 .. T::MaxCollators::get();
<LicenseBond<T>>::put(T::Currency::minimum_balance());
- <DesiredCandidates<T>>::put(c);
+ <DesiredCollators<T>>::put(c);
frame_system::Pallet::<T>::set_block_number(0u32.into());
register_validators::<T>(c);
pallets/collator-selection/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// Original license:18// Copyright (C) 2021 Parity Technologies (UK) Ltd.19// SPDX-License-Identifier: Apache-2.02021// Licensed under the Apache License, Version 2.0 (the "License");22// you may not use this file except in compliance with the License.23// You may obtain a copy of the License at24//25// http://www.apache.org/licenses/LICENSE-2.026//27// Unless required by applicable law or agreed to in writing, software28// distributed under the License is distributed on an "AS IS" BASIS,29// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.30// See the License for the specific language governing permissions and31// limitations under the License.3233// todo:collator documentation34//! Collator Selection pallet.35//!36//! A pallet to manage collators in a parachain.37//!38//! ## Overview39//!40//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a41//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT42//! safety assumptions of the chosen set.43//!44//! ## Terminology45//!46//! - Collator: A parachain block producer.47//! - Bond: An amount of `Balance` _reserved_ for candidate registration.48//! - Invulnerable: An account guaranteed to be in the collator set.49//!50//! ## Implementation51//!52//! The final `Collators` are aggregated from two individual lists:53//!54//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be55//! collators.56//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as57//! a final collator.58//!59//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve60//! manner.61//!62//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates63//! fall below MinCandidates. This is for potential disaster recovery scenarios.64//!65//! ### Rewards66//!67//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the68//! collator who authored it receives:69//!70//! - Half the value of the Pot.71//! - Half the value of the transaction fees within the block. The other half of the transaction72//! fees are deposited into the Pot.73//!74//! To initiate rewards an ED needs to be transferred to the pot address.75//!76//! Note: Eventually the Pot distribution may be modified as discussed in77//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7879#![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 /// A convertor from collators id. Since this pallet does not have stash/controller, this is123 /// just identity.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 /// Configure the pallet by specifying the parameters and types on which it depends.132 #[pallet::config]133 pub trait Config: frame_system::Config {134 /// Overarching event type.135 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;136137 /// The currency mechanism.138 type Currency: ReservableCurrency<Self::AccountId>;139140 /// Origin that can dictate updating parameters of this pallet.141 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;142143 /// Account Identifier that holds the chain's treasury.144 type TreasuryAccountId: Get<Self::AccountId>;145146 /// Account Identifier from which the internal Pot is generated.147 type PotId: Get<PalletId>;148149 /// Maximum number of candidates that we should have. This is enforced in code.150 ///151 /// This does not take into account the invulnerables.152 type MaxCandidates: Get<u32>;153154 /// Minimum number of candidates that we should have. This is used for disaster recovery.155 ///156 /// This does not take into account the invulnerables.157 type MinCandidates: Get<u32>;158159 /// Maximum number of invulnerables. This is enforced in code.160 type MaxInvulnerables: Get<u32>;161162 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.163 type SlashRatio: Get<Perbill>;164165 /// A stable ID for a validator.166 type ValidatorId: Member + Parameter;167168 /// A conversion from account ID to validator ID.169 ///170 /// Its cost must be at most one storage read.171 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;172173 /// Validate a user is registered174 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;175176 /// The weight information of this pallet.177 type WeightInfo: WeightInfo;178 }179180 /// Basic information about a collation candidate.181 #[derive(182 PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,183 )]184 pub struct LicenseInfo<AccountId, Balance> {185 /// Account identifier.186 pub who: AccountId,187 /// Reserved deposit.188 pub deposit: Balance,189 }190191 #[pallet::pallet]192 #[pallet::generate_store(pub(super) trait Store)]193 pub struct Pallet<T>(_);194195 /// The invulnerable, fixed collators.196 #[pallet::storage]197 #[pallet::getter(fn invulnerables)]198 pub type Invulnerables<T: Config> =199 StorageValue<_, BoundedVec<T::AccountId, T::MaxInvulnerables>, ValueQuery>;200201 /// The (community) collation license holders.202 #[pallet::storage]203 #[pallet::getter(fn licenses)]204 pub type Licenses<T: Config> =205 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;206207 /// The (community, limited) collation candidates.208 #[pallet::storage]209 #[pallet::getter(fn candidates)]210 pub type Candidates<T: Config> = StorageValue<211 _,212 BoundedVec<T::AccountId, T::MaxCandidates>, //LicenseInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>, // license ID?213 ValueQuery,214 >;215216 /// Collator will be kicked if it does not produce a block within the threshold (does not apply to invulnerables).217 ///218 /// Should be a multiple of session or things will get inconsistent. todo:collator reword?219 #[pallet::storage]220 #[pallet::getter(fn kick_threshold)]221 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;222223 /// Last block authored by collator.224 #[pallet::storage]225 #[pallet::getter(fn last_authored_block)]226 pub type LastAuthoredBlock<T: Config> =227 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;228229 /// Desired number of candidates.230 ///231 /// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct.232 #[pallet::storage]233 #[pallet::getter(fn desired_candidates)]234 pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;235236 /// Fixed amount to deposit to become a collator.237 ///238 /// When a collator calls `leave_intent` they immediately receive the deposit back.239 #[pallet::storage]240 #[pallet::getter(fn license_bond)]241 pub type LicenseBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;242243 #[pallet::genesis_config]244 pub struct GenesisConfig<T: Config> {245 pub invulnerables: Vec<T::AccountId>,246 pub license_bond: BalanceOf<T>,247 pub kick_threshold: T::BlockNumber,248 pub desired_candidates: u32,249 }250251 #[cfg(feature = "std")]252 impl<T: Config> Default for GenesisConfig<T> {253 fn default() -> Self {254 Self {255 invulnerables: Default::default(),256 license_bond: Default::default(),257 kick_threshold: T::BlockNumber::one(),258 desired_candidates: Default::default(),259 }260 }261 }262263 #[pallet::genesis_build]264 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {265 fn build(&self) {266 let duplicate_invulnerables = self267 .invulnerables268 .iter()269 .collect::<std::collections::BTreeSet<_>>();270 assert!(271 duplicate_invulnerables.len() == self.invulnerables.len(),272 "duplicate invulnerables in genesis."273 );274275 let bounded_invulnerables =276 BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())277 .expect("genesis invulnerables are more than T::MaxInvulnerables");278 assert!(279 T::MaxCandidates::get() >= self.desired_candidates,280 "genesis desired_candidates are more than T::MaxCandidates",281 );282283 <DesiredCandidates<T>>::put(&self.desired_candidates);284 <LicenseBond<T>>::put(&self.license_bond);285 <KickThreshold<T>>::put(&self.kick_threshold);286 <Invulnerables<T>>::put(bounded_invulnerables);287 }288 }289290 #[pallet::event]291 #[pallet::generate_deposit(pub(super) fn deposit_event)]292 pub enum Event<T: Config> {293 NewDesiredCandidates {294 desired_candidates: u32,295 },296 NewLicenseBond {297 bond_amount: BalanceOf<T>,298 },299 NewKickThreshold {300 length_in_blocks: T::BlockNumber,301 },302 InvulnerableAdded {303 invulnerable: T::AccountId,304 },305 InvulnerableRemoved {306 invulnerable: T::AccountId,307 },308 LicenseObtained {309 account_id: T::AccountId,310 deposit: BalanceOf<T>,311 },312 LicenseForfeited {313 account_id: T::AccountId,314 deposit_returned: BalanceOf<T>,315 },316 CandidateAdded {317 account_id: T::AccountId,318 },319 CandidateRemoved {320 account_id: T::AccountId,321 },322 }323324 // Errors inform users that something went wrong.325 #[pallet::error]326 pub enum Error<T> {327 /// Too many candidates328 TooManyCandidates,329 /// Too few candidates330 TooFewCandidates,331 /// Unknown error332 Unknown,333 /// Permission issue334 Permission,335 /// User already holds license to collate336 AlreadyLicenseHolder,337 /// User does not hold a license to collate338 NoLicense,339 /// User is already a candidate340 AlreadyCandidate,341 /// User is not a candidate342 NotCandidate,343 /// Too many invulnerables344 TooManyInvulnerables,345 /// Too few invulnerables346 TooFewInvulnerables,347 /// User is already an Invulnerable348 AlreadyInvulnerable,349 /// User is not an Invulnerable350 NotInvulnerable,351 /// Account has no associated validator ID352 NoAssociatedValidatorId,353 /// Validator ID is not yet registered354 ValidatorNotRegistered,355 }356357 #[pallet::hooks]358 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}359360 #[pallet::call]361 impl<T: Config> Pallet<T> {362 /// Add a collator to the list of invulnerable (fixed) collators.363 #[pallet::weight(T::WeightInfo::set_invulnerables(1 as u32))] // todo:collator weight364 pub fn add_invulnerable(365 origin: OriginFor<T>,366 new: T::AccountId,367 ) -> DispatchResultWithPostInfo {368 T::UpdateOrigin::ensure_origin(origin)?;369370 // check if the new invulnerable has associated validator keys before it is added371 let validator_key = T::ValidatorIdOf::convert(new.clone())372 .ok_or(Error::<T>::NoAssociatedValidatorId)?;373 ensure!(374 T::ValidatorRegistration::is_registered(&validator_key),375 Error::<T>::ValidatorNotRegistered376 );377 // ensure!(!Self::invulnerables().contains(&new), Error::<T>::AlreadyInvulnerable);378 if Self::invulnerables().contains(&new) {379 return Ok(().into());380 }381382 // todo:collator check license holders, release moneys, promotion!383 // force_release_license? Error::<T>::lreadyLicenseHolder?384385 <Invulnerables<T>>::try_append(new.clone())386 .map_err(|_| Error::<T>::TooManyInvulnerables)?;387 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });388 Ok(().into())389 }390391 /// Remove a collator from the list of invulnerable (fixed) collators.392 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] // todo:collator weight393 pub fn remove_invulnerable(394 origin: OriginFor<T>,395 who: T::AccountId,396 ) -> DispatchResultWithPostInfo {397 T::UpdateOrigin::ensure_origin(origin)?;398399 // let index = Self::invulnerables().into_iter().position(|r| r == who).ok_or(Error::<T>::NotInvulnerable)?;400 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {401 if invulnerables.len() <= 1 {402 return Err(Error::<T>::TooFewInvulnerables.into());403 }404405 let index = invulnerables406 .into_iter()407 .position(|r| *r == who)408 .ok_or(Error::<T>::NotInvulnerable)?;409 invulnerables.remove(index);410 Ok(())411 })?;412 /*let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)413 .map_err(|_| Error::<T>::TooManyInvulnerables)?;414415 <Invulnerables<T>>::put(&bounded_invulnerables);*/416 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });417 Ok(().into())418 }419420 /// Set the ideal number of collators (not including the invulnerables).421 /// If lowering this number, then the number of running collators could be higher than this figure.422 /// Aside from that edge case, there should be no other way to have more collators than the desired number.423 #[pallet::weight(T::WeightInfo::set_desired_candidates())]424 pub fn set_desired_candidates(425 origin: OriginFor<T>,426 max: u32,427 ) -> DispatchResultWithPostInfo {428 T::UpdateOrigin::ensure_origin(origin)?;429 // we trust origin calls, this is just a for more accurate benchmarking430 if max > T::MaxCandidates::get() {431 log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");432 }433 <DesiredCandidates<T>>::put(&max);434 Self::deposit_event(Event::NewDesiredCandidates {435 desired_candidates: max,436 });437 Ok(().into())438 }439440 /// Set the candidacy bond amount.441 #[pallet::weight(T::WeightInfo::set_license_bond())]442 pub fn set_license_bond(443 origin: OriginFor<T>,444 bond: BalanceOf<T>,445 ) -> DispatchResultWithPostInfo {446 T::UpdateOrigin::ensure_origin(origin)?;447 <LicenseBond<T>>::put(&bond);448 Self::deposit_event(Event::NewLicenseBond { bond_amount: bond });449 Ok(().into())450 }451452 /// Set the length of the kick threshold.453 /// Note that if the length is not a multiple of the session period, it might get inconsistent.454 #[pallet::weight(T::WeightInfo::set_license_bond())] // todo:collator weight455 pub fn set_kick_threshold(456 origin: OriginFor<T>,457 kick_threshold: T::BlockNumber,458 ) -> DispatchResultWithPostInfo {459 T::UpdateOrigin::ensure_origin(origin)?;460 // todo:collator insert something to guarantee consistency?461 <KickThreshold<T>>::put(kick_threshold);462 Self::deposit_event(Event::NewKickThreshold {463 length_in_blocks: kick_threshold,464 });465 Ok(().into())466 }467468 /// Purchase a license on block collation for this account.469 /// It does not make it a collator candidate, use `onboard` afterward. The account must470 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.471 ///472 /// This call is not available to `Invulnerable` collators.473 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] // todo:collator weight474 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {475 // register_as_candidate476 let who = ensure_signed(origin)?;477478 if Licenses::<T>::contains_key(&who) {479 return Ok(().into());480 }481482 ensure!(483 !Self::invulnerables().contains(&who),484 Error::<T>::AlreadyInvulnerable485 );486487 let validator_key = T::ValidatorIdOf::convert(who.clone())488 .ok_or(Error::<T>::NoAssociatedValidatorId)?;489 ensure!(490 T::ValidatorRegistration::is_registered(&validator_key),491 Error::<T>::ValidatorNotRegistered492 );493494 let deposit = Self::license_bond();495 // First authored block is current block plus kick threshold to handle session delay496 /*let incoming = LicenseInfo {497 who: who.clone(),498 deposit,499 };*/500501 T::Currency::reserve(&who, deposit)?;502 Licenses::<T>::insert(who.clone(), deposit);503504 /*let current_count =505 <Licenses<T>>::try_mutate(|licenses| -> Result<usize, DispatchError> {506 if T::OriginPrivilegeCmp::cmp_privilege(&origin, &scheduled.origin) {507 return Err(BadOrigin.into());508 }509 if candidates.iter().any(|candidate| *candidate == who) {510 Err(Error::<T>::AlreadyLicenseHolder)?511 } else {512 T::Currency::reserve(&who, deposit)?;513 candidates514 .try_push(incoming)515 .map_err(|_| Error::<T>::TooManyCandidates)?;516 <LastAuthoredBlock<T>>::insert(517 who.clone(),518 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),519 );520 Ok(candidates.len())521 }522 })?;*/523524 Self::deposit_event(Event::LicenseObtained {525 account_id: who,526 deposit,527 });528 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())529 }530531 /// Register this account as a candidate for collators for next sessions.532 /// The account must already hold a license, and cannot offboard immediately during a session.533 ///534 /// This call is not available to `Invulnerable` collators.535 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] // todo:collator weight536 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {537 // register_as_candidate538 let who = ensure_signed(origin)?;539540 // ensure the user obtained the license.541 ensure!(Licenses::<T>::contains_key(&who), Error::<T>::NoLicense);542 // ensure we are below limit.543 let length = <Candidates<T>>::decode_len().unwrap_or_default();544 ensure!(545 (length as u32) < Self::desired_candidates(),546 Error::<T>::TooManyCandidates547 );548 // todo:collator really need it?549 ensure!(550 !Self::invulnerables().contains(&who),551 Error::<T>::AlreadyInvulnerable552 );553554 let deposit = Self::license_bond();555 // First authored block is current block plus kick threshold to handle session delay556 /*let incoming = LicenseInfo {557 who: who.clone(),558 deposit,559 };*/560561 let current_count =562 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {563 if candidates.iter().any(|candidate| *candidate == who) {564 Err(Error::<T>::AlreadyCandidate)?565 } else {566 T::Currency::reserve(&who, deposit)?;567 candidates568 .try_push(who.clone())569 .map_err(|_| Error::<T>::TooManyCandidates)?;570 <LastAuthoredBlock<T>>::insert(571 who.clone(),572 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),573 );574 Ok(candidates.len())575 }576 })?;577578 Self::deposit_event(Event::CandidateAdded { account_id: who });579 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())580 }581582 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on583 /// session change. The license to `onboard` later at any other time will remain.584 ///585 /// This call will fail if the total number of candidates would drop below `MinCandidates`. todo:collator maybe not586 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] // todo:collator weight587 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {588 // leave_intent589 let who = ensure_signed(origin)?;590 // todo:collator invulnerables and candidates should count against min candidates together591 ensure!(592 Self::candidates().len() as u32 > T::MinCandidates::get(),593 Error::<T>::TooFewCandidates594 );595 let current_count = Self::try_remove_candidate(&who)?;596597 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())598 }599600 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.601 ///602 /// This call is not available to `Invulnerable` collators.603 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] // todo:collator weight604 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {605 // leave_intent606 let who = ensure_signed(origin)?;607 // let current_count = Self::try_remove_candidate(&who, false)?;608 Self::try_release_license(&who, false)?;609610 Ok(().into())611 }612613 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.614 /// Note that the collator can only leave on session change.615 /// The `LicenseBond` will be unreserved and returned immediately.616 ///617 /// This call is not available to `Invulnerable` collators.618 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] // todo:collator weight619 pub fn force_release_license(620 origin: OriginFor<T>,621 who: T::AccountId,622 ) -> DispatchResultWithPostInfo {623 // leave_intent624 T::UpdateOrigin::ensure_origin(origin)?;625626 let current_count = Self::try_remove_candidate(&who)?;627 Self::try_release_license(&who, false)?;628629 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight630 }631 }632633 impl<T: Config> Pallet<T> {634 /// Get a unique, inaccessible account id from the `PotId`.635 pub fn account_id() -> T::AccountId {636 T::PotId::get().into_account_truncating()637 }638639 /// Removes a candidate from the collator pool for the next session if they exist.640 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {641 let current_count =642 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {643 let index = candidates644 .iter()645 .position(|candidate| *candidate == *who)646 .ok_or(Error::<T>::NotCandidate)?;647 candidates.remove(index);648 <LastAuthoredBlock<T>>::remove(who.clone());649 Ok(candidates.len())650 })?;651 Self::deposit_event(Event::CandidateRemoved {652 account_id: who.clone(),653 });654 Ok(current_count)655 }656657 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.658 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {659 let mut deposit_returned = BalanceOf::<T>::default();660 Licenses::<T>::try_mutate_exists(&who, |deposit| -> DispatchResult {661 if let Some(deposit) = deposit.take() {662 if should_slash {663 let slashed = T::SlashRatio::get() * deposit;664 let remaining = deposit - slashed;665666 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);667 //T::Currency::unreserve(who, remaining);668 deposit_returned = remaining;669670 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);671 } else {672 //T::Currency::unreserve(who, deposit);673 deposit_returned = deposit;674 }675676 T::Currency::unreserve(who, deposit_returned);677 Ok(())678 } else {679 Err(Error::<T>::NoLicense.into())680 }681 })?;682 Self::deposit_event(Event::LicenseForfeited {683 account_id: who.clone(),684 deposit_returned,685 });686 Ok(())687 }688689 /// Assemble the current set of candidates and invulnerables into the next collator set.690 ///691 /// This is done on the fly, as frequent as we are told to do so, as the session manager.692 pub fn assemble_collators(693 candidates: BoundedVec<T::AccountId, T::MaxCandidates>,694 ) -> Vec<T::AccountId> {695 let mut collators = Self::invulnerables().to_vec();696 collators.extend(candidates);697 collators698 }699700 /// Kicks out candidates that did not produce a block in the kick threshold701 /// and **confiscates** their deposits to the treasury.702 pub fn kick_stale_candidates(703 candidates: BoundedVec<T::AccountId, T::MaxCandidates>, //LicenseInfo<T::AccountId, BalanceOf<T>>704 ) -> BoundedVec<T::AccountId, T::MaxCandidates> {705 let now = frame_system::Pallet::<T>::block_number();706 let kick_threshold = Self::kick_threshold();707 candidates708 .into_iter()709 .filter_map(|c| {710 let last_block = <LastAuthoredBlock<T>>::get(c.clone());711 let since_last = now.saturating_sub(last_block);712 if since_last < kick_threshold ||713 Self::candidates().len() as u32 <= T::MinCandidates::get()714 {715 Some(c)716 } else {717 let outcome = Self::try_remove_candidate(&c);718 if let Err(why) = outcome {719 log::warn!("Failed to remove candidate {:?}", why);720 debug_assert!(false, "failed to remove candidate {:?}", why);721 return None;722 }723 let outcome = Self::try_release_license(&c, true);724 if let Err(why) = outcome {725 log::warn!("Failed to release license {:?}", why);726 debug_assert!(false, "failed to release license {:?}", why);727 }728 None729 }730 })731 .collect::<Vec<_>>()732 .try_into()733 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")734 }735 }736737 /// Keep track of number of authored blocks per authority, uncles are counted as well since738 /// they're a valid proof of being online.739 impl<T: Config + pallet_authorship::Config>740 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>741 {742 fn note_author(author: T::AccountId) {743 let pot = Self::account_id();744 // assumes an ED will be sent to pot.745 let reward = T::Currency::free_balance(&pot)746 .checked_sub(&T::Currency::minimum_balance())747 .unwrap_or_else(Zero::zero)748 .div(2u32.into());749 // `reward` is half of pot account minus ED, this should never fail.750 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);751 debug_assert!(_success.is_ok());752 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());753754 frame_system::Pallet::<T>::register_extra_weight_unchecked(755 T::WeightInfo::note_author(),756 DispatchClass::Mandatory,757 );758 }759760 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {761 //TODO can we ignore this?762 }763 }764765 /// Play the role of the session manager.766 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {767 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {768 log::info!(769 "assembling new collators for new session {} at #{:?}",770 index,771 <frame_system::Pallet<T>>::block_number(),772 );773774 let candidates = Self::candidates();775 let candidates_len_before = candidates.len();776 let active_candidates = Self::kick_stale_candidates(candidates);777 let removed = candidates_len_before - active_candidates.len();778 let result = Self::assemble_collators(active_candidates);779780 frame_system::Pallet::<T>::register_extra_weight_unchecked(781 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),782 DispatchClass::Mandatory,783 );784 Some(result)785 }786 fn start_session(_: SessionIndex) {787 // we don't care.788 }789 fn end_session(_: SessionIndex) {790 // we don't care.791 }792 }793}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// Original license:18// Copyright (C) 2021 Parity Technologies (UK) Ltd.19// SPDX-License-Identifier: Apache-2.02021// Licensed under the Apache License, Version 2.0 (the "License");22// you may not use this file except in compliance with the License.23// You may obtain a copy of the License at24//25// http://www.apache.org/licenses/LICENSE-2.026//27// Unless required by applicable law or agreed to in writing, software28// distributed under the License is distributed on an "AS IS" BASIS,29// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.30// See the License for the specific language governing permissions and31// limitations under the License.3233// todo:collator documentation34//! Collator Selection pallet.35//!36//! A pallet to manage collators in a parachain.37//!38//! ## Overview39//!40//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a41//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT42//! safety assumptions of the chosen set.43//!44//! ## Terminology45//!46//! - Collator: A parachain block producer.47//! - Bond: An amount of `Balance` _reserved_ for candidate registration.48//! - Invulnerable: An account guaranteed to be in the collator set.49//!50//! ## Implementation51//!52//! The final `Collators` are aggregated from two individual lists:53//!54//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be55//! collators.56//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as57//! a final collator.58//!59//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve60//! manner.61//!62//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates63//! fall below MinCandidates. This is for potential disaster recovery scenarios.64//!65//! ### Rewards66//!67//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the68//! collator who authored it receives:69//!70//! - Half the value of the Pot.71//! - Half the value of the transaction fees within the block. The other half of the transaction72//! fees are deposited into the Pot.73//!74//! To initiate rewards an ED needs to be transferred to the pot address.75//!76//! Note: Eventually the Pot distribution may be modified as discussed in77//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7879#![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 /// A convertor from collators id. Since this pallet does not have stash/controller, this is120 /// just identity.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 /// Configure the pallet by specifying the parameters and types on which it depends.129 #[pallet::config]130 pub trait Config: frame_system::Config {131 /// Overarching event type.132 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;133134 /// The currency mechanism.135 type Currency: ReservableCurrency<Self::AccountId>;136137 /// Origin that can dictate updating parameters of this pallet.138 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;139140 /// Account Identifier that holds the chain's treasury.141 type TreasuryAccountId: Get<Self::AccountId>;142143 /// Account Identifier from which the internal Pot is generated.144 type PotId: Get<PalletId>;145146 /// Maximum number of candidates and invulnerables that we should have. This is enforced in code.147 type MaxCollators: Get<u32>;148149 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.150 type SlashRatio: Get<Perbill>;151152 /// A stable ID for a validator.153 type ValidatorId: Member + Parameter;154155 /// A conversion from account ID to validator ID.156 ///157 /// Its cost must be at most one storage read.158 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;159160 /// Validate a user is registered161 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;162163 /// The weight information of this pallet.164 type WeightInfo: WeightInfo;165 }166167 #[pallet::pallet]168 #[pallet::generate_store(pub(super) trait Store)]169 pub struct Pallet<T>(_);170171 /// The invulnerable, fixed collators.172 #[pallet::storage]173 #[pallet::getter(fn invulnerables)]174 pub type Invulnerables<T: Config> =175 StorageValue<_, BoundedVec<T::AccountId, T::MaxCollators>, ValueQuery>;176177 /// The (community) collation license holders.178 #[pallet::storage]179 #[pallet::getter(fn licenses)]180 pub type Licenses<T: Config> =181 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;182183 /// The (community, limited) collation candidates.184 #[pallet::storage]185 #[pallet::getter(fn candidates)]186 pub type Candidates<T: Config> = StorageValue<187 _,188 BoundedVec<T::AccountId, T::MaxCollators>, //LicenseInfo<T::AccountId, BalanceOf<T>>, T::MaxCollators>, // license ID?189 ValueQuery,190 >;191192 /// Collator will be kicked if it does not produce a block within the threshold (does not apply to invulnerables).193 ///194 /// Should be a multiple of session or things will get inconsistent. todo:collator reword?195 #[pallet::storage]196 #[pallet::getter(fn kick_threshold)]197 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;198199 /// Last block authored by collator.200 #[pallet::storage]201 #[pallet::getter(fn last_authored_block)]202 pub type LastAuthoredBlock<T: Config> =203 StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;204205 /// Desired number of candidates.206 ///207 /// This should ideally always be less than [`Config::MaxCollators`] for weights to be correct.208 #[pallet::storage]209 #[pallet::getter(fn desired_collators)]210 pub type DesiredCollators<T> = StorageValue<_, u32, ValueQuery>;211212 /// Fixed amount to deposit to become a collator.213 ///214 /// When a collator calls `leave_intent` they immediately receive the deposit back.215 #[pallet::storage]216 #[pallet::getter(fn license_bond)]217 pub type LicenseBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;218219 #[pallet::genesis_config]220 pub struct GenesisConfig<T: Config> {221 pub invulnerables: Vec<T::AccountId>,222 pub license_bond: BalanceOf<T>,223 pub kick_threshold: T::BlockNumber,224 pub desired_collators: u32,225 }226227 #[cfg(feature = "std")]228 impl<T: Config> Default for GenesisConfig<T> {229 fn default() -> Self {230 Self {231 invulnerables: Default::default(),232 license_bond: Default::default(),233 kick_threshold: T::BlockNumber::one(),234 desired_collators: Default::default(),235 }236 }237 }238239 #[pallet::genesis_build]240 impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {241 fn build(&self) {242 let duplicate_invulnerables = self243 .invulnerables244 .iter()245 .collect::<std::collections::BTreeSet<_>>();246 assert!(247 duplicate_invulnerables.len() == self.invulnerables.len(),248 "duplicate invulnerables in genesis."249 );250251 let bounded_invulnerables =252 BoundedVec::<_, T::MaxCollators>::try_from(self.invulnerables.clone())253 .expect("genesis invulnerables are more than T::MaxCollators");254 assert!(255 T::MaxCollators::get() >= self.desired_collators,256 "genesis desired_collators are more than T::MaxCollators",257 );258259 <DesiredCollators<T>>::put(self.desired_collators);260 <LicenseBond<T>>::put(self.license_bond);261 <KickThreshold<T>>::put(self.kick_threshold);262 <Invulnerables<T>>::put(bounded_invulnerables);263 }264 }265266 #[pallet::event]267 #[pallet::generate_deposit(pub(super) fn deposit_event)]268 pub enum Event<T: Config> {269 NewDesiredCollators {270 desired_collators: u32,271 },272 NewLicenseBond {273 bond_amount: BalanceOf<T>,274 },275 NewKickThreshold {276 length_in_blocks: T::BlockNumber,277 },278 InvulnerableAdded {279 invulnerable: T::AccountId,280 },281 InvulnerableRemoved {282 invulnerable: T::AccountId,283 },284 LicenseObtained {285 account_id: T::AccountId,286 deposit: BalanceOf<T>,287 },288 LicenseForfeited {289 account_id: T::AccountId,290 deposit_returned: BalanceOf<T>,291 },292 CandidateAdded {293 account_id: T::AccountId,294 },295 CandidateRemoved {296 account_id: T::AccountId,297 },298 }299300 // Errors inform users that something went wrong.301 #[pallet::error]302 pub enum Error<T> {303 /// Too many candidates304 TooManyCandidates,305 /// Unknown error306 Unknown,307 /// Permission issue308 Permission,309 /// User already holds license to collate310 AlreadyHoldingLicense,311 /// User does not hold a license to collate312 NoLicense,313 /// User is already a candidate314 AlreadyCandidate,315 /// User is not a candidate316 NotCandidate,317 /// Too many invulnerables318 TooManyInvulnerables,319 /// Too few invulnerables320 TooFewInvulnerables,321 /// User is already an Invulnerable322 AlreadyInvulnerable,323 /// User is not an Invulnerable324 NotInvulnerable,325 /// Account has no associated validator ID326 NoAssociatedValidatorId,327 /// Validator ID is not yet registered328 ValidatorNotRegistered,329 }330331 #[pallet::hooks]332 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}333334 #[pallet::call]335 impl<T: Config> Pallet<T> {336 /// Add a collator to the list of invulnerable (fixed) collators.337 #[pallet::weight(T::WeightInfo::set_invulnerables(1u32))] // todo:collator weight338 pub fn add_invulnerable(339 origin: OriginFor<T>,340 new: T::AccountId,341 ) -> DispatchResultWithPostInfo {342 T::UpdateOrigin::ensure_origin(origin)?;343344 // check if the new invulnerable has associated validator keys before it is added345 let validator_key = T::ValidatorIdOf::convert(new.clone())346 .ok_or(Error::<T>::NoAssociatedValidatorId)?;347 ensure!(348 T::ValidatorRegistration::is_registered(&validator_key),349 Error::<T>::ValidatorNotRegistered350 );351 // ensure!(!Self::invulnerables().contains(&new), Error::<T>::AlreadyInvulnerable);352 if Self::invulnerables().contains(&new) {353 return Ok(().into());354 }355356 <Invulnerables<T>>::try_append(new.clone())357 .map_err(|_| Error::<T>::TooManyInvulnerables)?;358359 // try to offboard the new invulnerable if it was a collator candidate before360 let _ = Self::try_remove_candidate(&new);361362 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });363 Ok(().into())364 }365366 /// Remove a collator from the list of invulnerable (fixed) collators.367 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] // todo:collator weight368 pub fn remove_invulnerable(369 origin: OriginFor<T>,370 who: T::AccountId,371 ) -> DispatchResultWithPostInfo {372 T::UpdateOrigin::ensure_origin(origin)?;373374 // let index = Self::invulnerables().into_iter().position(|r| r == who).ok_or(Error::<T>::NotInvulnerable)?;375 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {376 if invulnerables.len() <= 1 {377 return Err(Error::<T>::TooFewInvulnerables.into());378 }379380 let index = invulnerables381 .into_iter()382 .position(|r| *r == who)383 .ok_or(Error::<T>::NotInvulnerable)?;384 invulnerables.remove(index);385 Ok(())386 })?;387 /*let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)388 .map_err(|_| Error::<T>::TooManyInvulnerables)?;389390 <Invulnerables<T>>::put(&bounded_invulnerables);*/391 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });392 Ok(().into())393 }394395 /// Set the ideal number of collators. If lowering this number,396 /// then the number of running collators could be higher than this figure.397 /// Aside from that edge case, there should be no other way to have more collators than the desired number.398 #[pallet::weight(T::WeightInfo::set_desired_collators())]399 pub fn set_desired_collators(origin: OriginFor<T>, max: u32) -> DispatchResultWithPostInfo {400 T::UpdateOrigin::ensure_origin(origin)?;401 // we trust origin calls, this is just a for more accurate benchmarking402 if max > T::MaxCollators::get() {403 log::warn!("max > T::MaxCollators; you might need to run benchmarks again");404 }405 <DesiredCollators<T>>::put(max);406 Self::deposit_event(Event::NewDesiredCollators {407 desired_collators: max,408 });409 Ok(().into())410 }411412 /// Set the candidacy bond amount.413 #[pallet::weight(T::WeightInfo::set_license_bond())]414 pub fn set_license_bond(415 origin: OriginFor<T>,416 bond: BalanceOf<T>,417 ) -> DispatchResultWithPostInfo {418 T::UpdateOrigin::ensure_origin(origin)?;419 <LicenseBond<T>>::put(bond);420 Self::deposit_event(Event::NewLicenseBond { bond_amount: bond });421 Ok(().into())422 }423424 /// Set the length of the kick threshold.425 /// Note that if the length is not a multiple of the session period, it might get inconsistent.426 #[pallet::weight(T::WeightInfo::set_license_bond())] // todo:collator weight427 pub fn set_kick_threshold(428 origin: OriginFor<T>,429 kick_threshold: T::BlockNumber,430 ) -> DispatchResultWithPostInfo {431 T::UpdateOrigin::ensure_origin(origin)?;432 // todo:collator insert something to guarantee consistency?433 <KickThreshold<T>>::put(kick_threshold);434 Self::deposit_event(Event::NewKickThreshold {435 length_in_blocks: kick_threshold,436 });437 Ok(().into())438 }439440 /// Purchase a license on block collation for this account.441 /// It does not make it a collator candidate, use `onboard` afterward. The account must442 /// (a) already have registered session keys and (b) be able to reserve the `LicenseBond`.443 ///444 /// This call is not available to `Invulnerable` collators.445 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] // todo:collator weight446 pub fn get_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {447 // register_as_candidate448 let who = ensure_signed(origin)?;449450 if Licenses::<T>::contains_key(&who) {451 return Err(Error::<T>::AlreadyHoldingLicense.into());452 }453454 /*ensure!(455 !Self::invulnerables().contains(&who),456 Error::<T>::AlreadyInvulnerable457 );*/458459 let validator_key = T::ValidatorIdOf::convert(who.clone())460 .ok_or(Error::<T>::NoAssociatedValidatorId)?;461 ensure!(462 T::ValidatorRegistration::is_registered(&validator_key),463 Error::<T>::ValidatorNotRegistered464 );465466 let deposit = Self::license_bond();467 // First authored block is current block plus kick threshold to handle session delay468 /*let incoming = LicenseInfo {469 who: who.clone(),470 deposit,471 };*/472473 T::Currency::reserve(&who, deposit)?;474 Licenses::<T>::insert(who.clone(), deposit);475476 /*let current_count =477 <Licenses<T>>::try_mutate(|licenses| -> Result<usize, DispatchError> {478 if T::OriginPrivilegeCmp::cmp_privilege(&origin, &scheduled.origin) {479 return Err(BadOrigin.into());480 }481 if candidates.iter().any(|candidate| *candidate == who) {482 Err(Error::<T>::AlreadyHoldingLicense)?483 } else {484 T::Currency::reserve(&who, deposit)?;485 candidates486 .try_push(incoming)487 .map_err(|_| Error::<T>::TooManyCandidates)?;488 <LastAuthoredBlock<T>>::insert(489 who.clone(),490 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),491 );492 Ok(candidates.len())493 }494 })?;*/495496 Self::deposit_event(Event::LicenseObtained {497 account_id: who,498 deposit,499 });500 Ok(().into()) // Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())501 }502503 /// Register this account as a candidate for collators for next sessions.504 /// The account must already hold a license, and cannot offboard immediately during a session.505 ///506 /// This call is not available to `Invulnerable` collators.507 #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCollators::get()))] // todo:collator weight508 pub fn onboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {509 // register_as_candidate510 let who = ensure_signed(origin)?;511512 // ensure the user obtained the license.513 ensure!(Licenses::<T>::contains_key(&who), Error::<T>::NoLicense);514 // ensure we are below limit.515 let length = <Candidates<T>>::decode_len().unwrap_or_default()516 + <Invulnerables<T>>::decode_len().unwrap_or_default();517 ensure!(518 (length as u32) < Self::desired_collators(),519 Error::<T>::TooManyCandidates520 );521 // todo:collator really need it?522 ensure!(523 !Self::invulnerables().contains(&who),524 Error::<T>::AlreadyInvulnerable525 );526527 /*let incoming = LicenseInfo {528 who: who.clone(),529 deposit,530 };*/531532 let current_count =533 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {534 if candidates.iter().any(|candidate| *candidate == who) {535 Err(Error::<T>::AlreadyCandidate)?536 } else {537 candidates538 .try_push(who.clone())539 .map_err(|_| Error::<T>::TooManyCandidates)?;540 // First authored block is current block plus kick threshold to handle session delay541 <LastAuthoredBlock<T>>::insert(542 who.clone(),543 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),544 );545 Ok(candidates.len())546 }547 })?;548549 Self::deposit_event(Event::CandidateAdded { account_id: who });550 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())551 }552553 /// Deregister `origin` as a collator candidate. Note that the collator can only leave on554 /// session change. The license to `onboard` later at any other time will remain.555 ///556 /// This call will fail if the total number of candidates would drop below `MinCandidates`. todo:collator maybe not557 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight558 pub fn offboard(origin: OriginFor<T>) -> DispatchResultWithPostInfo {559 // leave_intent560 let who = ensure_signed(origin)?;561 /* todo:collator invulnerables and candidates should count against min candidates together562 ensure!(563 Self::candidates().len() as u32 > T::MinCandidates::get(),564 Error::<T>::TooFewCandidates565 );*/566 let current_count = Self::try_remove_candidate(&who)?;567568 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight569 }570571 /// Forfeit `origin`'s own license. The `LicenseBond` will be unreserved immediately.572 ///573 /// This call is not available to `Invulnerable` collators.574 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight575 pub fn release_license(origin: OriginFor<T>) -> DispatchResultWithPostInfo {576 // leave_intent577 let who = ensure_signed(origin)?;578579 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;580581 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight582 }583584 /// Force deregister `origin` as a collator candidate as a governing authority, and revoke its license.585 /// Note that the collator can only leave on session change.586 /// The `LicenseBond` will be unreserved and returned immediately.587 ///588 /// This call is not available to `Invulnerable` collators.589 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCollators::get()))] // todo:collator weight590 pub fn force_revoke_license(591 origin: OriginFor<T>,592 who: T::AccountId,593 ) -> DispatchResultWithPostInfo {594 // leave_intent595 T::UpdateOrigin::ensure_origin(origin)?;596597 let current_count = Self::try_remove_candidate_and_release_license(&who, false, true)?;598599 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) // todo:collator weight600 }601 }602603 impl<T: Config> Pallet<T> {604 /// Get a unique, inaccessible account id from the `PotId`.605 pub fn account_id() -> T::AccountId {606 T::PotId::get().into_account_truncating()607 }608609 fn try_remove_candidate_and_release_license(610 who: &T::AccountId,611 should_slash: bool,612 ignore_if_not_candidate: bool,613 ) -> Result<usize, DispatchError> {614 let current_count = Self::try_remove_candidate(who);615 let current_count = if ignore_if_not_candidate616 && current_count == Err(Error::<T>::NotCandidate.into())617 {618 <Candidates<T>>::decode_len().unwrap_or_default()619 } else {620 current_count?621 };622 Self::try_release_license(who, should_slash)?;623 Ok(current_count)624 }625626 /// Removes a candidate from the collator pool for the next session if they exist.627 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {628 let current_count =629 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {630 let index = candidates631 .iter()632 .position(|candidate| *candidate == *who)633 .ok_or(Error::<T>::NotCandidate)?;634 candidates.remove(index);635 <LastAuthoredBlock<T>>::remove(who.clone());636 Ok(candidates.len())637 })?;638 Self::deposit_event(Event::CandidateRemoved {639 account_id: who.clone(),640 });641 Ok(current_count)642 }643644 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.645 fn try_release_license(who: &T::AccountId, should_slash: bool) -> DispatchResult {646 let mut deposit_returned = BalanceOf::<T>::default();647 Licenses::<T>::try_mutate_exists(who, |deposit| -> DispatchResult {648 if let Some(deposit) = deposit.take() {649 if should_slash {650 let slashed = T::SlashRatio::get() * deposit;651 let remaining = deposit - slashed;652653 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);654 //T::Currency::unreserve(who, remaining);655 deposit_returned = remaining;656657 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);658 } else {659 //T::Currency::unreserve(who, deposit);660 deposit_returned = deposit;661 }662663 T::Currency::unreserve(who, deposit_returned);664 Ok(())665 } else {666 Err(Error::<T>::NoLicense.into())667 }668 })?;669 Self::deposit_event(Event::LicenseForfeited {670 account_id: who.clone(),671 deposit_returned,672 });673 Ok(())674 }675676 /// Assemble the current set of candidates and invulnerables into the next collator set.677 ///678 /// This is done on the fly, as frequent as we are told to do so, as the session manager.679 pub fn assemble_collators(680 candidates: BoundedVec<T::AccountId, T::MaxCollators>,681 ) -> Vec<T::AccountId> {682 let mut collators = Self::invulnerables().to_vec();683 collators.extend(candidates);684 collators685 }686687 /// Kicks out candidates that did not produce a block in the kick threshold688 /// and **confiscates** their deposits to the treasury.689 pub fn kick_stale_candidates(690 candidates: BoundedVec<T::AccountId, T::MaxCollators>, //LicenseInfo<T::AccountId, BalanceOf<T>>691 ) -> BoundedVec<T::AccountId, T::MaxCollators> {692 let now = frame_system::Pallet::<T>::block_number();693 let kick_threshold = Self::kick_threshold();694 candidates695 .into_iter()696 .filter_map(|c| {697 let last_block = <LastAuthoredBlock<T>>::get(c.clone());698 let since_last = now.saturating_sub(last_block);699 if since_last < kick_threshold {700 Some(c)701 } else {702 let outcome = Self::try_remove_candidate_and_release_license(&c, true, false);703 if let Err(why) = outcome {704 log::warn!("Failed to kick collator and release license {:?}", why);705 debug_assert!(false, "failed to kick collator and release license {why:?}");706 }707 None708 }709 })710 .collect::<Vec<_>>()711 .try_into()712 .expect("filter_map operation can't result in a bounded vec larger than its original; qed")713 }714 }715716 /// Keep track of number of authored blocks per authority, uncles are counted as well since717 /// they're a valid proof of being online.718 impl<T: Config + pallet_authorship::Config>719 pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>720 {721 fn note_author(author: T::AccountId) {722 let pot = Self::account_id();723 // assumes an ED will be sent to pot.724 let reward = T::Currency::free_balance(&pot)725 .checked_sub(&T::Currency::minimum_balance())726 .unwrap_or_else(Zero::zero)727 .div(2u32.into());728 // `reward` is half of pot account minus ED, this should never fail.729 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);730 debug_assert!(_success.is_ok());731 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());732733 frame_system::Pallet::<T>::register_extra_weight_unchecked(734 T::WeightInfo::note_author(),735 DispatchClass::Mandatory,736 );737 }738739 fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {740 //TODO can we ignore this?741 }742 }743744 /// Play the role of the session manager.745 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {746 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {747 log::info!(748 "assembling new collators for new session {} at #{:?}",749 index,750 <frame_system::Pallet<T>>::block_number(),751 );752753 let candidates = Self::candidates();754 let candidates_len_before = candidates.len();755 let active_candidates = Self::kick_stale_candidates(candidates);756 let removed = candidates_len_before - active_candidates.len();757 let result = Self::assemble_collators(active_candidates);758759 frame_system::Pallet::<T>::register_extra_weight_unchecked(760 T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),761 DispatchClass::Mandatory,762 );763 Some(result)764 }765 fn start_session(_: SessionIndex) {766 // we don't care.767 }768 fn end_session(_: SessionIndex) {769 // we don't care.770 }771 }772}pallets/collator-selection/src/mock.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/mock.rs
+++ b/pallets/collator-selection/src/mock.rs
@@ -206,9 +206,7 @@
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCandidates: u32 = 20;
- pub const MaxInvulnerables: u32 = 20;
- pub const MinCandidates: u32 = 1;
+ pub const MaxCollators: u32 = 20;
pub const MaxAuthorities: u32 = 100_000;
pub const SlashRatio: Perbill = Perbill::one();
}
@@ -230,9 +228,7 @@
type Currency = Balances;
type UpdateOrigin = EnsureSignedBy<RootAccount, u64>;
type PotId = PotId;
- type MaxCandidates = MaxCandidates;
- type MinCandidates = MinCandidates;
- type MaxInvulnerables = MaxInvulnerables;
+ type MaxCollators = MaxCollators;
// type KickThreshold = Period;
type SlashRatio = SlashRatio;
type TreasuryAccountId = ();
@@ -263,9 +259,9 @@
})
.collect::<Vec<_>>();
let collator_selection = collator_selection::GenesisConfig::<Test> {
- desired_candidates: 2,
+ desired_collators: 5,
license_bond: 10,
- kick_threshold: 1,
+ kick_threshold: 10,
invulnerables,
};
let session = pallet_session::GenesisConfig::<Test> { keys };
pallets/collator-selection/src/tests.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/tests.rs
+++ b/pallets/collator-selection/src/tests.rs
@@ -31,7 +31,7 @@
// limitations under the License.
use crate as collator_selection;
-use crate::{mock::*, LicenseInfo, Error};
+use crate::{mock::*, Error};
use frame_support::{
assert_noop, assert_ok,
traits::{Currency, GenesisBuild, OnInitialize},
@@ -39,10 +39,19 @@
use pallet_balances::Error as BalancesError;
use sp_runtime::traits::BadOrigin;
+fn get_license_and_onboard(account_id: <Test as frame_system::Config>::AccountId) {
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(
+ account_id
+ )));
+ assert_ok!(CollatorSelection::onboard(RuntimeOrigin::signed(
+ account_id
+ )));
+}
+
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
- assert_eq!(CollatorSelection::desired_candidates(), 2);
+ assert_eq!(CollatorSelection::desired_collators(), 5);
assert_eq!(CollatorSelection::license_bond(), 10);
assert!(CollatorSelection::candidates().is_empty());
@@ -51,6 +60,7 @@
}
// todo:collator add more tests later
+// invulnerable after onboard + invulnerables can bypass desired_candidates
#[test]
fn it_should_add_invulnerables() {
@@ -112,21 +122,21 @@
}
#[test]
-fn set_desired_candidates_works() {
+fn set_desired_collators_works() {
new_test_ext().execute_with(|| {
// given
- assert_eq!(CollatorSelection::desired_candidates(), 2);
+ assert_eq!(CollatorSelection::desired_collators(), 5);
// can set
- assert_ok!(CollatorSelection::set_desired_candidates(
+ assert_ok!(CollatorSelection::set_desired_collators(
RuntimeOrigin::signed(RootAccount::get()),
7
));
- assert_eq!(CollatorSelection::desired_candidates(), 7);
+ assert_eq!(CollatorSelection::desired_collators(), 7);
// rejects bad origin
assert_noop!(
- CollatorSelection::set_desired_candidates(RuntimeOrigin::signed(1), 8),
+ CollatorSelection::set_desired_collators(RuntimeOrigin::signed(1), 8),
BadOrigin
);
});
@@ -154,166 +164,246 @@
}
#[test]
-fn cannot_register_candidate_if_too_many() {
+fn cannot_onboard_candidate_with_no_license() {
new_test_ext().execute_with(|| {
- // reset desired candidates:
- <crate::DesiredCandidates<Test>>::put(0);
+ // can't onboard a candidate who did not get a license.
+ assert_noop!(
+ CollatorSelection::onboard(RuntimeOrigin::signed(3)),
+ Error::<Test>::NoLicense,
+ );
+
+ // but give it a license and welcome aboard.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(3)));
+ assert_ok!(CollatorSelection::onboard(RuntimeOrigin::signed(3)));
+ })
+}
+
+#[test]
+fn cannot_onboard_candidate_if_too_many() {
+ new_test_ext().execute_with(|| {
+ // reset desired candidates
+ <crate::DesiredCollators<Test>>::put(0);
+
+ // can still get a license.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(4)));
// can't accept anyone anymore.
assert_noop!(
- CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)),
+ CollatorSelection::onboard(RuntimeOrigin::signed(4)),
Error::<Test>::TooManyCandidates,
);
- // reset desired candidates:
- <crate::DesiredCandidates<Test>>::put(1);
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(4)
- ));
+ // reset desired candidates to invulnerables + 1
+ <crate::DesiredCollators<Test>>::put(3);
+ assert_ok!(CollatorSelection::onboard(RuntimeOrigin::signed(4)));
- // but no more
+ // but no more.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(5)));
assert_noop!(
- CollatorSelection::register_as_candidate(RuntimeOrigin::signed(5)),
+ CollatorSelection::onboard(RuntimeOrigin::signed(5)),
Error::<Test>::TooManyCandidates,
);
})
}
#[test]
-fn cannot_unregister_candidate_if_too_few() {
+fn cannot_obtain_license_if_keys_not_registered() {
new_test_ext().execute_with(|| {
- // reset desired candidates:
- <crate::DesiredCandidates<Test>>::put(1);
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(4)
- ));
-
- // can not remove too few
+ // can't 7 because keys not registered.
assert_noop!(
- CollatorSelection::leave_intent(RuntimeOrigin::signed(4)),
- Error::<Test>::TooFewCandidates,
+ CollatorSelection::get_license(RuntimeOrigin::signed(7)),
+ Error::<Test>::ValidatorNotRegistered
);
})
}
#[test]
-fn cannot_register_as_candidate_if_invulnerable() {
+fn cannot_obtain_license_if_poor() {
new_test_ext().execute_with(|| {
- assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
+ assert_eq!(Balances::free_balance(&3), 100);
+ assert_eq!(Balances::free_balance(&33), 0);
- // can't 1 because it is invulnerable.
- assert_noop!(
- CollatorSelection::register_as_candidate(RuntimeOrigin::signed(1)),
- Error::<Test>::AlreadyInvulnerable,
- );
- })
-}
+ // works
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(3)));
-#[test]
-fn cannot_register_as_candidate_if_keys_not_registered() {
- new_test_ext().execute_with(|| {
- // can't 7 because keys not registered.
+ // poor
assert_noop!(
- CollatorSelection::register_as_candidate(RuntimeOrigin::signed(7)),
- Error::<Test>::ValidatorNotRegistered
+ CollatorSelection::get_license(RuntimeOrigin::signed(33)),
+ BalancesError::<Test>::InsufficientBalance,
);
- })
+ });
}
#[test]
-fn cannot_register_dupe_candidate() {
+fn cannot_onboard_dupe_candidate() {
new_test_ext().execute_with(|| {
// can add 3 as candidate
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
- ));
- let addition = LicenseInfo {
- who: 3,
- deposit: 10,
- };
- assert_eq!(CollatorSelection::candidates(), vec![addition]);
+ get_license_and_onboard(3);
+ assert_eq!(CollatorSelection::licenses(3), 10);
+ assert_eq!(CollatorSelection::candidates(), vec![3]);
assert_eq!(CollatorSelection::last_authored_block(3), 10);
assert_eq!(Balances::free_balance(3), 90);
// but no more
assert_noop!(
- CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)),
+ CollatorSelection::get_license(RuntimeOrigin::signed(3)),
+ Error::<Test>::AlreadyHoldingLicense,
+ );
+ assert_noop!(
+ CollatorSelection::onboard(RuntimeOrigin::signed(3)),
Error::<Test>::AlreadyCandidate,
);
})
}
#[test]
-fn cannot_register_as_candidate_if_poor() {
+fn becoming_candidate_works() {
new_test_ext().execute_with(|| {
+ // given
+ assert_eq!(CollatorSelection::desired_collators(), 5);
+ assert_eq!(CollatorSelection::license_bond(), 10);
+ assert_eq!(CollatorSelection::candidates(), Vec::new());
+ assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
+
+ // take two endowed, non-invulnerables accounts.
assert_eq!(Balances::free_balance(&3), 100);
- assert_eq!(Balances::free_balance(&33), 0);
+ assert_eq!(Balances::free_balance(&4), 100);
- // works
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
- ));
+ get_license_and_onboard(3);
+ get_license_and_onboard(4);
+
+ assert_eq!(Balances::free_balance(&3), 90);
+ assert_eq!(Balances::free_balance(&4), 90);
- // poor
- assert_noop!(
- CollatorSelection::register_as_candidate(RuntimeOrigin::signed(33)),
- BalancesError::<Test>::InsufficientBalance,
- );
+ assert_eq!(CollatorSelection::candidates().len(), 2);
});
}
#[test]
-fn register_as_candidate_works() {
+fn cannot_become_candidate_if_invulnerable() {
new_test_ext().execute_with(|| {
- // given
- assert_eq!(CollatorSelection::desired_candidates(), 2);
- assert_eq!(CollatorSelection::license_bond(), 10);
- assert_eq!(CollatorSelection::candidates(), Vec::new());
assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
- // take two endowed, non-invulnerables accounts.
- assert_eq!(Balances::free_balance(&3), 100);
- assert_eq!(Balances::free_balance(&4), 100);
+ // can obtain a license even if is invulnerable.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(1)));
+ // but cannot onboard
+ assert_noop!(
+ CollatorSelection::onboard(RuntimeOrigin::signed(1)),
+ Error::<Test>::AlreadyInvulnerable,
+ );
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
+ // get a license and then become invulnerable.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(3)));
+ assert_ok!(CollatorSelection::add_invulnerable(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 3
));
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(4)
+ assert_noop!(
+ CollatorSelection::onboard(RuntimeOrigin::signed(3)),
+ Error::<Test>::AlreadyInvulnerable,
+ );
+ })
+}
+
+#[test]
+fn can_become_invulnerable_if_candidate() {
+ new_test_ext().execute_with(|| {
+ // become a candidate and then become invulnerable.
+ get_license_and_onboard(3);
+ assert_eq!(CollatorSelection::candidates(), vec![3]);
+
+ assert_ok!(CollatorSelection::add_invulnerable(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 3
));
+ // should exclude from candidates, but not revoke the license
+ assert_eq!(CollatorSelection::candidates(), vec![]);
+ assert_eq!(CollatorSelection::licenses(3), 10);
+ assert_eq!(Balances::free_balance(3), 90);
+ });
+}
- assert_eq!(Balances::free_balance(&3), 90);
- assert_eq!(Balances::free_balance(&4), 90);
+#[test]
+fn offboard() {
+ new_test_ext().execute_with(|| {
+ // register a candidate.
+ get_license_and_onboard(3);
+ assert_eq!(Balances::free_balance(3), 90);
+
+ // cannot leave if holds license but not yet candidate.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(4)));
+ assert_noop!(
+ CollatorSelection::offboard(RuntimeOrigin::signed(4)),
+ Error::<Test>::NotCandidate
+ );
+ // cannot leave if does not hold license.
+ assert_noop!(
+ CollatorSelection::offboard(RuntimeOrigin::signed(5)),
+ Error::<Test>::NotCandidate
+ );
- assert_eq!(CollatorSelection::candidates().len(), 2);
+ // bond is returned - only after releasing the license
+ assert_ok!(CollatorSelection::offboard(RuntimeOrigin::signed(3)));
+ assert_eq!(Balances::free_balance(3), 90);
+ assert_eq!(CollatorSelection::last_authored_block(3), 0);
+ assert_ok!(CollatorSelection::release_license(RuntimeOrigin::signed(3)));
+ assert_eq!(Balances::free_balance(3), 100);
});
}
#[test]
-fn leave_intent() {
+fn release_license() {
new_test_ext().execute_with(|| {
+ // obtain a license to collate and reserve the bond.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(3)));
+ assert_eq!(Balances::free_balance(3), 90);
+
+ // release the license and get the bond back.
+ assert_ok!(CollatorSelection::release_license(RuntimeOrigin::signed(3)));
+ assert_eq!(Balances::free_balance(3), 100);
+
// register a candidate.
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
- ));
+ get_license_and_onboard(3);
assert_eq!(Balances::free_balance(3), 90);
- // register too so can leave above min candidates
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(5)
- ));
- assert_eq!(Balances::free_balance(5), 90);
+ // can release license even if onboarded.
+ assert_ok!(CollatorSelection::release_license(RuntimeOrigin::signed(3)));
+ assert_eq!(Balances::free_balance(3), 100);
+ assert_eq!(CollatorSelection::candidates(), vec![]);
+ });
+}
+
+#[test]
+fn force_revoke_license() {
+ new_test_ext().execute_with(|| {
+ // obtain a license to collate and reserve the bond.
+ assert_ok!(CollatorSelection::get_license(RuntimeOrigin::signed(3)));
+ assert_eq!(Balances::free_balance(3), 90);
- // cannot leave if not candidate.
+ // cannot execute the operation as non-root
assert_noop!(
- CollatorSelection::leave_intent(RuntimeOrigin::signed(4)),
- Error::<Test>::NotCandidate
+ CollatorSelection::force_revoke_license(RuntimeOrigin::signed(3), 3),
+ BadOrigin
);
- // bond is returned
- assert_ok!(CollatorSelection::leave_intent(RuntimeOrigin::signed(3)));
+ // release the license and get the bond back.
+ assert_ok!(CollatorSelection::force_revoke_license(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 3
+ ));
+ assert_eq!(Balances::free_balance(3), 100);
+
+ // register a candidate.
+ get_license_and_onboard(3);
+ assert_eq!(Balances::free_balance(3), 90);
+
+ // can release license even if onboarded.
+ assert_ok!(CollatorSelection::force_revoke_license(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 3
+ ));
assert_eq!(Balances::free_balance(3), 100);
- assert_eq!(CollatorSelection::last_authored_block(3), 0);
+ assert_eq!(CollatorSelection::candidates(), vec![]);
});
}
@@ -325,18 +415,11 @@
// 4 is the default author.
assert_eq!(Balances::free_balance(4), 100);
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(4)
- ));
+ get_license_and_onboard(4);
// triggers `note_author`
Authorship::on_initialize(1);
- let collator = LicenseInfo {
- who: 4,
- deposit: 10,
- };
-
- assert_eq!(CollatorSelection::candidates(), vec![collator]);
+ assert_eq!(CollatorSelection::candidates(), vec![4]);
assert_eq!(CollatorSelection::last_authored_block(4), 0);
// half of the pot goes to the collator who's the author (4 in tests).
@@ -355,18 +438,11 @@
Balances::make_free_balance_be(&CollatorSelection::account_id(), 5);
// 4 is the default author.
assert_eq!(Balances::free_balance(4), 100);
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(4)
- ));
+ get_license_and_onboard(4);
// triggers `note_author`
Authorship::on_initialize(1);
- let collator = LicenseInfo {
- who: 4,
- deposit: 10,
- };
-
- assert_eq!(CollatorSelection::candidates(), vec![collator]);
+ assert_eq!(CollatorSelection::candidates(), vec![4]);
assert_eq!(CollatorSelection::last_authored_block(4), 0);
// Nothing received
assert_eq!(Balances::free_balance(4), 90);
@@ -389,9 +465,7 @@
assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
// add a new collator
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
- ));
+ get_license_and_onboard(5);
// session won't see this.
assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
@@ -410,7 +484,7 @@
initialize_to_block(20);
assert_eq!(SessionChangeBlock::get(), 20);
// changed are now reflected to session handlers.
- assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3]);
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 5]);
});
}
@@ -418,64 +492,28 @@
fn kick_mechanism() {
new_test_ext().execute_with(|| {
// add a new collator
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
- ));
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(4)
- ));
+ get_license_and_onboard(3);
+ get_license_and_onboard(4);
+
initialize_to_block(10);
assert_eq!(CollatorSelection::candidates().len(), 2);
+
initialize_to_block(20);
assert_eq!(SessionChangeBlock::get(), 20);
// 4 authored this block, gets to stay 3 was kicked
assert_eq!(CollatorSelection::candidates().len(), 1);
// 3 will be kicked after 1 session delay
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]);
- let collator = LicenseInfo {
- who: 4,
- deposit: 10,
- };
- assert_eq!(CollatorSelection::candidates(), vec![collator]);
- assert_eq!(CollatorSelection::kick_threshold(), 1);
+
+ assert_eq!(CollatorSelection::candidates(), vec![4]);
+ assert_eq!(CollatorSelection::kick_threshold(), 10);
assert_eq!(CollatorSelection::last_authored_block(4), 20);
+
initialize_to_block(30);
// 3 gets kicked after 1 session delay
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 4]);
- // kicked collator gets funds back
- assert_eq!(Balances::free_balance(3), 100);
- });
-}
-
-#[test]
-fn should_not_kick_mechanism_too_few() {
- new_test_ext().execute_with(|| {
- // add a new collator
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(3)
- ));
- assert_ok!(CollatorSelection::register_as_candidate(
- RuntimeOrigin::signed(5)
- ));
- initialize_to_block(10);
- assert_eq!(CollatorSelection::candidates().len(), 2);
- initialize_to_block(20);
- assert_eq!(SessionChangeBlock::get(), 20);
- // 4 authored this block, 5 gets to stay too few 3 was kicked
- assert_eq!(CollatorSelection::candidates().len(), 1);
- // 3 will be kicked after 1 session delay
- assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 5]);
- let collator = LicenseInfo {
- who: 5,
- deposit: 10,
- };
- assert_eq!(CollatorSelection::candidates(), vec![collator]);
- assert_eq!(CollatorSelection::last_authored_block(4), 20);
- initialize_to_block(30);
- // 3 gets kicked after 1 session delay
- assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 5]);
- // kicked collator gets funds back
- assert_eq!(Balances::free_balance(3), 100);
+ // kicked collator gets their funds slashed, the deposit going to treasury
+ assert_eq!(Balances::free_balance(3), 90);
});
}
@@ -489,9 +527,9 @@
let invulnerables = vec![1, 1];
let collator_selection = collator_selection::GenesisConfig::<Test> {
- desired_candidates: 2,
+ desired_collators: 5,
license_bond: 10,
- kick_threshold: 1,
+ kick_threshold: 10,
invulnerables,
};
// collator selection must be initialized before session.
pallets/collator-selection/src/weights.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/weights.rs
+++ b/pallets/collator-selection/src/weights.rs
@@ -45,7 +45,7 @@
// The weight info trait for `pallet_collator_selection`.
pub trait WeightInfo {
fn set_invulnerables(_b: u32) -> Weight;
- fn set_desired_candidates() -> Weight;
+ fn set_desired_collators() -> Weight;
fn set_license_bond() -> Weight;
fn register_as_candidate(_c: u32) -> Weight;
fn leave_intent(_c: u32) -> Weight;
@@ -62,7 +62,7 @@
.saturating_add(Weight::from_ref_time(68_000 as u64).saturating_mul(b as u64))
.saturating_add(T::DbWeight::get().writes(1 as u64))
}
- fn set_desired_candidates() -> Weight {
+ fn set_desired_collators() -> Weight {
Weight::from_ref_time(16_363_000 as u64).saturating_add(T::DbWeight::get().writes(1 as u64))
}
fn set_license_bond() -> Weight {
@@ -108,7 +108,7 @@
.saturating_add(Weight::from_ref_time(68_000 as u64).saturating_mul(b as u64))
.saturating_add(RocksDbWeight::get().writes(1 as u64))
}
- fn set_desired_candidates() -> Weight {
+ fn set_desired_collators() -> Weight {
Weight::from_ref_time(16_363_000 as u64)
.saturating_add(RocksDbWeight::get().writes(1 as u64))
}
primitives/common/src/constants.rsdiffbeforeafterboth--- a/primitives/common/src/constants.rs
+++ b/primitives/common/src/constants.rs
@@ -45,9 +45,9 @@
/// Minimum balance required to create or keep an account open.
pub const EXISTENTIAL_DEPOSIT: u128 = 0;
/// Amount of Balance reserved for candidate registration.
-pub const GENESIS_LICENSE_BOND: u128 = EXISTENTIAL_DEPOSIT;
+pub const GENESIS_LICENSE_BOND: u128 = 1_000_000_000_000 * UNIQUE;
/// How long a periodic session lasts in blocks.
-pub const SESSION_LENGTH: BlockNumber = MINUTES;
+pub const SESSION_LENGTH: BlockNumber = HOURS;
// Targeting 0.1 UNQ per transfer
pub const WEIGHT_TO_FEE_COEFF: u32 = /*<weight2fee>*/175_199_920/*</weight2fee>*/;
runtime/common/config/pallets/collator_selection.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/collator_selection.rs
+++ b/runtime/common/config/pallets/collator_selection.rs
@@ -55,9 +55,7 @@
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCandidates: u32 = 30; // todo:collator 30 collator slots - 3 planned invulnerables
- pub const MinCandidates: u32 = 1;
- pub const MaxInvulnerables: u32 = 30;
+ pub const MaxCollators: u32 = 10;
pub const SlashRatio: Perbill = Perbill::from_percent(100);
}
@@ -68,9 +66,7 @@
type UpdateOrigin = EnsureRoot<AccountId>;
type TreasuryAccountId = TreasuryAccountId;
type PotId = PotId;
- type MaxCandidates = MaxCandidates;
- type MinCandidates = MinCandidates;
- type MaxInvulnerables = MaxInvulnerables;
+ type MaxCollators = MaxCollators;
// todo:collator kick threshold should be in storage and configured only by root -- or rather UpdateOrigin
type SlashRatio = SlashRatio;
type ValidatorId = <Self as frame_system::Config>::AccountId;
runtime/common/mod.rsdiffbeforeafterboth--- a/runtime/common/mod.rs
+++ b/runtime/common/mod.rs
@@ -192,7 +192,7 @@
};
use pallet_session::SessionManager;
use up_common::constants::GENESIS_LICENSE_BOND;
- use crate::config::pallets::collator_selection::MaxInvulnerables;
+ use crate::config::pallets::collator_selection::MaxCollators;
let mut weight = <Runtime as frame_system::Config>::DbWeight::get().reads(1);
@@ -231,17 +231,17 @@
})
.collect::<Vec<_>>();
- let bounded_invulnerables = BoundedVec::<_, MaxInvulnerables>::try_from(
+ let bounded_invulnerables = BoundedVec::<_, MaxCollators>::try_from(
invulnerables
.iter()
.cloned()
.map(|(acc, _)| acc)
.collect::<Vec<_>>(),
)
- .expect("Existing collators/invulnerables are more than MaxInvulnerables");
+ .expect("Existing collators/invulnerables are more than MaxCollators");
<pallet_collator_selection::Invulnerables<Runtime>>::put(bounded_invulnerables);
- <pallet_collator_selection::DesiredCandidates<Runtime>>::put(0);
+ <pallet_collator_selection::DesiredCollators<Runtime>>::put(MaxCollators::get());
<pallet_collator_selection::LicenseBond<Runtime>>::put(GENESIS_LICENSE_BOND);
let keys = invulnerables
runtime/common/tests/mod.rsdiffbeforeafterboth--- a/runtime/common/tests/mod.rs
+++ b/runtime/common/tests/mod.rs
@@ -18,7 +18,7 @@
use sp_core::{Public, Pair};
use sp_std::vec;
use up_common::types::AuraId;
-use crate::{GenesisConfig, ParachainInfoConfig, AuraConfig};
+use crate::{GenesisConfig, ParachainInfoConfig};
pub mod xcm;
@@ -28,7 +28,61 @@
.public()
}
+#[cfg(feature = "collator-selection")]
+fn new_test_ext(para_id: u32) -> sp_io::TestExternalities {
+ use sp_core::{sr25519};
+ use sp_runtime::traits::{IdentifyAccount, Verify};
+ use crate::{AccountId, Signature, SessionKeys, CollatorSelectionConfig, SessionConfig};
+
+ type AccountPublic = <Signature as Verify>::Signer;
+
+ fn get_account_id_from_seed<TPublic: Public>(seed: &str) -> AccountId
+ where
+ AccountPublic: From<<TPublic::Pair as Pair>::Public>,
+ {
+ AccountPublic::from(get_from_seed::<TPublic>(seed)).into_account()
+ }
+
+ let accounts = vec!["Alice", "Bob"];
+ let keys = accounts
+ .iter()
+ .map(|&acc| {
+ let account_id = get_account_id_from_seed::<sr25519::Public>(acc);
+ (
+ account_id.clone(),
+ account_id,
+ SessionKeys {
+ aura: get_from_seed::<AuraId>(acc),
+ },
+ )
+ })
+ .collect::<Vec<_>>();
+ let invulnerables = accounts
+ .iter()
+ .map(|acc| get_account_id_from_seed::<sr25519::Public>(acc))
+ .collect::<Vec<_>>();
+
+ let cfg = GenesisConfig {
+ collator_selection: CollatorSelectionConfig {
+ desired_collators: 2,
+ license_bond: 10,
+ kick_threshold: 10,
+ invulnerables,
+ },
+ session: SessionConfig { keys },
+ parachain_info: ParachainInfoConfig {
+ parachain_id: para_id.into(),
+ },
+ ..GenesisConfig::default()
+ };
+
+ cfg.build_storage().unwrap().into()
+}
+
+#[cfg(not(feature = "collator-selection"))]
fn new_test_ext(para_id: u32) -> sp_io::TestExternalities {
+ use crate::AuraConfig;
+
let cfg = GenesisConfig {
aura: AuraConfig {
authorities: vec![