git.delta.rocks / unique-network / refs/commits / 1e14fe80093b

difftreelog

feat(collator-selection) add+remove invulnerable methods + tests + miscellaneous changes before more refactoring

Fahrrader2022-12-21parent: #9b89504.patch.diff
in: master

11 files changed

modifiednode/cli/src/chain_spec.rsdiffbeforeafterboth
--- a/node/cli/src/chain_spec.rs
+++ b/node/cli/src/chain_spec.rs
@@ -24,7 +24,7 @@
 use serde_json::map::Map;
 
 use up_common::types::opaque::*;
-use up_common::constants::GENESIS_CANDIDACY_BOND;
+use up_common::constants::{GENESIS_CANDIDACY_BOND, SESSION_LENGTH};
 
 #[cfg(feature = "unique-runtime")]
 pub use unique_runtime as default_runtime;
@@ -197,6 +197,7 @@
 					.map(|(acc, _)| acc)
 					.collect(),
 				candidacy_bond: GENESIS_CANDIDACY_BOND,
+				kick_threshold: SESSION_LENGTH,
 				..Default::default()
 			},
 			session: SessionConfig {
modifiedpallets/collator-selection/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/collator-selection/src/benchmarking.rs
+++ b/pallets/collator-selection/src/benchmarking.rs
@@ -112,8 +112,13 @@
 }
 
 fn register_candidates<T: Config>(count: u32) {
-	let candidates = (0..count).map(|c| account("candidate", c, SEED)).collect::<Vec<_>>();
-	assert!(<CandidacyBond<T>>::get() > 0u32.into(), "Bond cannot be zero!");
+	let candidates = (0..count)
+		.map(|c| account("candidate", c, SEED))
+		.collect::<Vec<_>>();
+	assert!(
+		<CandidacyBond<T>>::get() > 0u32.into(),
+		"Bond cannot be zero!"
+	);
 
 	for who in candidates {
 		T::Currency::make_free_balance_be(&who, <CandidacyBond<T>>::get() * 2u32.into());
@@ -200,7 +205,8 @@
 		whitelist!(leaving);
 	}: _(RawOrigin::Signed(leaving.clone()))
 	verify {
-		assert_last_event::<T>(Event::CandidateRemoved{account_id: leaving}.into());
+		// todo:collator verify these
+		assert_last_event::<T>(Event::CandidateRemoved{account_id: leaving, deposit_returned: bond / 2u32.into() }.into());
 	}
 
 	// worse case is paying a non-existing candidate account.
@@ -272,4 +278,8 @@
 	}
 }
 
-impl_benchmark_test_suite!(CollatorSelection, crate::mock::new_test_ext(), crate::mock::Test,);
+impl_benchmark_test_suite!(
+	CollatorSelection,
+	crate::mock::new_test_ext(),
+	crate::mock::Test,
+);
modifiedpallets/collator-selection/src/lib.rsdiffbeforeafterboth
before · pallets/collator-selection/src/lib.rs
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//! Collator Selection pallet.34//!35//! A pallet to manage collators in a parachain.36//!37//! ## Overview38//!39//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a40//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT41//! safety assumptions of the chosen set.42//!43//! ## Terminology44//!45//! - Collator: A parachain block producer.46//! - Bond: An amount of `Balance` _reserved_ for candidate registration.47//! - Invulnerable: An account guaranteed to be in the collator set.48//!49//! ## Implementation50//!51//! The final `Collators` are aggregated from two individual lists:52//!53//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be54//!    collators.55//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as56//!    a final collator.57//!58//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve59//! manner.60//!61//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates62//! fall below MinCandidates. This is for potential disaster recovery scenarios.63//!64//! ### Rewards65//!66//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the67//! collator who authored it receives:68//!69//! - Half the value of the Pot.70//! - Half the value of the transaction fees within the block. The other half of the transaction71//!   fees are deposited into the Pot.72//!73//! To initiate rewards an ED needs to be transferred to the pot address.74//!75//! Note: Eventually the Pot distribution may be modified as discussed in76//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).7778#![cfg_attr(not(feature = "std"), no_std)]7980pub use pallet::*;8182#[cfg(test)]83mod mock;8485#[cfg(test)]86mod tests;8788#[cfg(feature = "runtime-benchmarks")]89mod benchmarking;90pub mod weights;9192#[frame_support::pallet]93pub mod pallet {94	pub use crate::weights::WeightInfo;95	use core::ops::Div;96	use frame_support::{97		dispatch::{DispatchClass, DispatchResultWithPostInfo},98		inherent::Vec,99		pallet_prelude::*,100		sp_runtime::{101			traits::{AccountIdConversion, CheckedSub, Saturating, Zero},102			RuntimeDebug,103		},104		traits::{105			Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,106			ValidatorRegistration,107		},108		BoundedVec, PalletId,109	};110	use frame_system::{pallet_prelude::*, Config as SystemConfig};111	use pallet_session::SessionManager;112	use sp_runtime::traits::Convert;113	use sp_staking::SessionIndex;114115	type BalanceOf<T> =116		<<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;117118	/// A convertor from collators id. Since this pallet does not have stash/controller, this is119	/// just identity.120	pub struct IdentityCollator;121	impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {122		fn convert(t: T) -> Option<T> {123			Some(t)124		}125	}126127	/// Configure the pallet by specifying the parameters and types on which it depends.128	#[pallet::config]129	pub trait Config: frame_system::Config {130		/// Overarching event type.131		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;132133		/// The currency mechanism.134		type Currency: ReservableCurrency<Self::AccountId>;135136		/// Origin that can dictate updating parameters of this pallet.137		type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;138139		/// Account Identifier from which the internal Pot is generated.140		type PotId: Get<PalletId>;141142		/// Maximum number of candidates that we should have. This is enforced in code.143		///144		/// This does not take into account the invulnerables.145		type MaxCandidates: Get<u32>;146147		/// Minimum number of candidates that we should have. This is used for disaster recovery.148		///149		/// This does not take into account the invulnerables.150		type MinCandidates: Get<u32>;151152		/// Maximum number of invulnerables. This is enforced in code.153		type MaxInvulnerables: Get<u32>;154155		// Will be kicked if block is not produced in threshold.156		type KickThreshold: Get<Self::BlockNumber>;157158		/// A stable ID for a validator.159		type ValidatorId: Member + Parameter;160161		/// A conversion from account ID to validator ID.162		///163		/// Its cost must be at most one storage read.164		type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;165166		/// Validate a user is registered167		type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;168169		/// The weight information of this pallet.170		type WeightInfo: WeightInfo;171	}172173	/// Basic information about a collation candidate.174	#[derive(175		PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,176	)]177	pub struct CandidateInfo<AccountId, Balance> {178		/// Account identifier.179		pub who: AccountId,180		/// Reserved deposit.181		pub deposit: Balance,182	}183184	#[pallet::pallet]185	#[pallet::generate_store(pub(super) trait Store)]186	pub struct Pallet<T>(_);187188	/// The invulnerable, fixed collators.189	#[pallet::storage]190	#[pallet::getter(fn invulnerables)]191	pub type Invulnerables<T: Config> =192		StorageValue<_, BoundedVec<T::AccountId, T::MaxInvulnerables>, ValueQuery>;193194	/// The (community, limited) collation candidates.195	#[pallet::storage]196	#[pallet::getter(fn candidates)]197	pub type Candidates<T: Config> = StorageValue<198		_,199		BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,200		ValueQuery,201	>;202203	/// Last block authored by collator.204	#[pallet::storage]205	#[pallet::getter(fn last_authored_block)]206	pub type LastAuthoredBlock<T: Config> =207		StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;208209	/// Desired number of candidates.210	///211	/// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct.212	#[pallet::storage]213	#[pallet::getter(fn desired_candidates)]214	pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;215216	/// Fixed amount to deposit to become a collator.217	///218	/// When a collator calls `leave_intent` they immediately receive the deposit back.219	#[pallet::storage]220	#[pallet::getter(fn candidacy_bond)]221	pub type CandidacyBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;222223	#[pallet::genesis_config]224	pub struct GenesisConfig<T: Config> {225		pub invulnerables: Vec<T::AccountId>,226		pub candidacy_bond: BalanceOf<T>,227		pub desired_candidates: u32,228	}229230	#[cfg(feature = "std")]231	impl<T: Config> Default for GenesisConfig<T> {232		fn default() -> Self {233			Self {234				invulnerables: Default::default(),235				candidacy_bond: Default::default(),236				desired_candidates: Default::default(),237			}238		}239	}240241	#[pallet::genesis_build]242	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {243		fn build(&self) {244			let duplicate_invulnerables =245				self.invulnerables.iter().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::MaxInvulnerables>::try_from(self.invulnerables.clone())253					.expect("genesis invulnerables are more than T::MaxInvulnerables");254			assert!(255				T::MaxCandidates::get() >= self.desired_candidates,256				"genesis desired_candidates are more than T::MaxCandidates",257			);258259			<DesiredCandidates<T>>::put(&self.desired_candidates);260			<CandidacyBond<T>>::put(&self.candidacy_bond);261			<Invulnerables<T>>::put(bounded_invulnerables);262		}263	}264265	#[pallet::event]266	#[pallet::generate_deposit(pub(super) fn deposit_event)]267	pub enum Event<T: Config> {268		NewInvulnerables { invulnerables: Vec<T::AccountId> },269		NewDesiredCandidates { desired_candidates: u32 },270		NewCandidacyBond { bond_amount: BalanceOf<T> },271		CandidateAdded { account_id: T::AccountId, deposit: BalanceOf<T> },272		CandidateRemoved { account_id: T::AccountId },273	}274275	// Errors inform users that something went wrong.276	#[pallet::error]277	pub enum Error<T> {278		/// Too many candidates279		TooManyCandidates,280		/// Too few candidates281		TooFewCandidates,282		/// Unknown error283		Unknown,284		/// Permission issue285		Permission,286		/// User is already a candidate287		AlreadyCandidate,288		/// User is not a candidate289		NotCandidate,290		/// Too many invulnerables291		TooManyInvulnerables,292		/// User is already an Invulnerable293		AlreadyInvulnerable,294		/// Account has no associated validator ID295		NoAssociatedValidatorId,296		/// Validator ID is not yet registered297		ValidatorNotRegistered,298	}299300	#[pallet::hooks]301	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}302303	#[pallet::call]304	impl<T: Config> Pallet<T> {305		/// Set the list of invulnerable (fixed) collators.306		#[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]307		pub fn set_invulnerables(308			origin: OriginFor<T>,309			new: Vec<T::AccountId>,310		) -> DispatchResultWithPostInfo {311			T::UpdateOrigin::ensure_origin(origin)?;312			let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)313				.map_err(|_| Error::<T>::TooManyInvulnerables)?;314315			// check if the invulnerables have associated validator keys before they are set316			for account_id in bounded_invulnerables.iter() {317				let validator_key = T::ValidatorIdOf::convert(account_id.clone())318					.ok_or(Error::<T>::NoAssociatedValidatorId)?;319				ensure!(320					T::ValidatorRegistration::is_registered(&validator_key),321					Error::<T>::ValidatorNotRegistered322				);323			}324325			<Invulnerables<T>>::put(&bounded_invulnerables);326			Self::deposit_event(Event::NewInvulnerables {327				invulnerables: bounded_invulnerables.to_vec(),328			});329			Ok(().into())330		}331332		/// Set the ideal number of collators (not including the invulnerables).333		/// If lowering this number, then the number of running collators could be higher than this figure.334		/// Aside from that edge case, there should be no other way to have more collators than the desired number.335		#[pallet::weight(T::WeightInfo::set_desired_candidates())]336		pub fn set_desired_candidates(337			origin: OriginFor<T>,338			max: u32,339		) -> DispatchResultWithPostInfo {340			T::UpdateOrigin::ensure_origin(origin)?;341			// we trust origin calls, this is just a for more accurate benchmarking342			if max > T::MaxCandidates::get() {343				log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");344			}345			<DesiredCandidates<T>>::put(&max);346			Self::deposit_event(Event::NewDesiredCandidates { desired_candidates: max });347			Ok(().into())348		}349350		/// Set the candidacy bond amount.351		#[pallet::weight(T::WeightInfo::set_candidacy_bond())]352		pub fn set_candidacy_bond(353			origin: OriginFor<T>,354			bond: BalanceOf<T>,355		) -> DispatchResultWithPostInfo {356			T::UpdateOrigin::ensure_origin(origin)?;357			<CandidacyBond<T>>::put(&bond);358			Self::deposit_event(Event::NewCandidacyBond { bond_amount: bond });359			Ok(().into())360		}361362		/// Register this account as a collator candidate. The account must (a) already have363		/// registered session keys and (b) be able to reserve the `CandidacyBond`.364		///365		/// This call is not available to `Invulnerable` collators.366		#[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))]367		pub fn register_as_candidate(origin: OriginFor<T>) -> DispatchResultWithPostInfo {368			let who = ensure_signed(origin)?;369370			// ensure we are below limit.371			let length = <Candidates<T>>::decode_len().unwrap_or_default();372			ensure!((length as u32) < Self::desired_candidates(), Error::<T>::TooManyCandidates);373			ensure!(!Self::invulnerables().contains(&who), Error::<T>::AlreadyInvulnerable);374375			let validator_key = T::ValidatorIdOf::convert(who.clone())376				.ok_or(Error::<T>::NoAssociatedValidatorId)?;377			ensure!(378				T::ValidatorRegistration::is_registered(&validator_key),379				Error::<T>::ValidatorNotRegistered380			);381382			let deposit = Self::candidacy_bond();383			// First authored block is current block plus kick threshold to handle session delay384			let incoming = CandidateInfo { who: who.clone(), deposit };385386			let current_count =387				<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {388					if candidates.iter().any(|candidate| candidate.who == who) {389						Err(Error::<T>::AlreadyCandidate)?390					} else {391						T::Currency::reserve(&who, deposit)?;392						candidates.try_push(incoming).map_err(|_| Error::<T>::TooManyCandidates)?;393						<LastAuthoredBlock<T>>::insert(394							who.clone(),395							frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),396						);397						Ok(candidates.len())398					}399				})?;400401			Self::deposit_event(Event::CandidateAdded { account_id: who, deposit });402			Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())403		}404405		/// Deregister `origin` as a collator candidate. Note that the collator can only leave on406		/// session change. The `CandidacyBond` will be unreserved immediately.407		///408		/// This call will fail if the total number of candidates would drop below `MinCandidates`.409		///410		/// This call is not available to `Invulnerable` collators.411		#[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]412		pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {413			let who = ensure_signed(origin)?;414			ensure!(415				Self::candidates().len() as u32 > T::MinCandidates::get(),416				Error::<T>::TooFewCandidates417			);418			let current_count = Self::try_remove_candidate(&who)?;419420			Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())421		}422	}423424	impl<T: Config> Pallet<T> {425		/// Get a unique, inaccessible account id from the `PotId`.426		pub fn account_id() -> T::AccountId {427			T::PotId::get().into_account_truncating()428		}429430		/// Removes a candidate if they exist and sends them back their deposit431		fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {432			let current_count =433				<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {434					let index = candidates435						.iter()436						.position(|candidate| candidate.who == *who)437						.ok_or(Error::<T>::NotCandidate)?;438					let candidate = candidates.remove(index);439					T::Currency::unreserve(who, candidate.deposit);440					<LastAuthoredBlock<T>>::remove(who.clone());441					Ok(candidates.len())442				})?;443			Self::deposit_event(Event::CandidateRemoved { account_id: who.clone() });444			Ok(current_count)445		}446447		/// Assemble the current set of candidates and invulnerables into the next collator set.448		///449		/// This is done on the fly, as frequent as we are told to do so, as the session manager.450		pub fn assemble_collators(451			candidates: BoundedVec<T::AccountId, T::MaxCandidates>,452		) -> Vec<T::AccountId> {453			let mut collators = Self::invulnerables().to_vec();454			collators.extend(candidates);455			collators456		}457458		/// Kicks out candidates that did not produce a block in the kick threshold459		/// and refund their deposits.460		pub fn kick_stale_candidates(461			candidates: BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,462		) -> BoundedVec<T::AccountId, T::MaxCandidates> {463			let now = frame_system::Pallet::<T>::block_number();464			let kick_threshold = T::KickThreshold::get();465			candidates466				.into_iter()467				.filter_map(|c| {468					let last_block = <LastAuthoredBlock<T>>::get(c.who.clone());469					let since_last = now.saturating_sub(last_block);470					if since_last < kick_threshold ||471						Self::candidates().len() as u32 <= T::MinCandidates::get()472					{473						Some(c.who)474					} else {475						let outcome = Self::try_remove_candidate(&c.who);476						if let Err(why) = outcome {477							log::warn!("Failed to remove candidate {:?}", why);478							debug_assert!(false, "failed to remove candidate {:?}", why);479						}480						None481					}482				})483				.collect::<Vec<_>>()484				.try_into()485				.expect("filter_map operation can't result in a bounded vec larger than its original; qed")486		}487	}488489	/// Keep track of number of authored blocks per authority, uncles are counted as well since490	/// they're a valid proof of being online.491	impl<T: Config + pallet_authorship::Config>492		pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Pallet<T>493	{494		fn note_author(author: T::AccountId) {495			let pot = Self::account_id();496			// assumes an ED will be sent to pot.497			let reward = T::Currency::free_balance(&pot)498				.checked_sub(&T::Currency::minimum_balance())499				.unwrap_or_else(Zero::zero)500				.div(2u32.into());501			// `reward` is half of pot account minus ED, this should never fail.502			let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);503			debug_assert!(_success.is_ok());504			<LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());505506			frame_system::Pallet::<T>::register_extra_weight_unchecked(507				T::WeightInfo::note_author(),508				DispatchClass::Mandatory,509			);510		}511512		fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {513			//TODO can we ignore this?514		}515	}516517	/// Play the role of the session manager.518	impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {519		fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {520			log::info!(521				"assembling new collators for new session {} at #{:?}",522				index,523				<frame_system::Pallet<T>>::block_number(),524			);525526			let candidates = Self::candidates();527			let candidates_len_before = candidates.len();528			let active_candidates = Self::kick_stale_candidates(candidates);529			let removed = candidates_len_before - active_candidates.len();530			let result = Self::assemble_collators(active_candidates);531532			frame_system::Pallet::<T>::register_extra_weight_unchecked(533				T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),534				DispatchClass::Mandatory,535			);536			Some(result)537		}538		fn start_session(_: SessionIndex) {539			// we don't care.540		}541		fn end_session(_: SessionIndex) {542			// we don't care.543		}544	}545}
modifiedpallets/collator-selection/src/mock.rsdiffbeforeafterboth
--- a/pallets/collator-selection/src/mock.rs
+++ b/pallets/collator-selection/src/mock.rs
@@ -43,7 +43,7 @@
 use sp_runtime::{
 	testing::{Header, UintAuthorityId},
 	traits::{BlakeTwo256, IdentityLookup, OpaqueKeys},
-	RuntimeAppPublic,
+	Perbill, RuntimeAppPublic,
 };
 
 type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
@@ -210,6 +210,7 @@
 	pub const MaxInvulnerables: u32 = 20;
 	pub const MinCandidates: u32 = 1;
 	pub const MaxAuthorities: u32 = 100_000;
+	pub const SlashRatio: Perbill = Perbill::one();
 }
 
 pub struct IsRegistered;
@@ -224,6 +225,7 @@
 }
 
 impl Config for Test {
+	// todo:collator mocks and stocks
 	type RuntimeEvent = RuntimeEvent;
 	type Currency = Balances;
 	type UpdateOrigin = EnsureSignedBy<RootAccount, u64>;
@@ -231,7 +233,9 @@
 	type MaxCandidates = MaxCandidates;
 	type MinCandidates = MinCandidates;
 	type MaxInvulnerables = MaxInvulnerables;
-	type KickThreshold = Period;
+	// type KickThreshold = Period;
+	type SlashRatio = SlashRatio;
+	type TreasuryAccountId = ();
 	type ValidatorId = <Self as frame_system::Config>::AccountId;
 	type ValidatorIdOf = IdentityCollator;
 	type ValidatorRegistration = IsRegistered;
@@ -240,17 +244,28 @@
 
 pub fn new_test_ext() -> sp_io::TestExternalities {
 	sp_tracing::try_init_simple();
-	let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
+	let mut t = frame_system::GenesisConfig::default()
+		.build_storage::<Test>()
+		.unwrap();
 	let invulnerables = vec![1, 2];
 
 	let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)];
 	let keys = balances
 		.iter()
-		.map(|&(i, _)| (i, i, MockSessionKeys { aura: UintAuthorityId(i) }))
+		.map(|&(i, _)| {
+			(
+				i,
+				i,
+				MockSessionKeys {
+					aura: UintAuthorityId(i),
+				},
+			)
+		})
 		.collect::<Vec<_>>();
 	let collator_selection = collator_selection::GenesisConfig::<Test> {
 		desired_candidates: 2,
 		candidacy_bond: 10,
+		kick_threshold: 1,
 		invulnerables,
 	};
 	let session = pallet_session::GenesisConfig::<Test> { keys };
modifiedpallets/collator-selection/src/tests.rsdiffbeforeafterboth
--- a/pallets/collator-selection/src/tests.rs
+++ b/pallets/collator-selection/src/tests.rs
@@ -113,7 +113,10 @@
 		assert_eq!(CollatorSelection::candidacy_bond(), 7);
 
 		// rejects bad origin.
-		assert_noop!(CollatorSelection::set_candidacy_bond(RuntimeOrigin::signed(1), 8), BadOrigin);
+		assert_noop!(
+			CollatorSelection::set_candidacy_bond(RuntimeOrigin::signed(1), 8),
+			BadOrigin
+		);
 	});
 }
 
@@ -131,7 +134,9 @@
 
 		// reset desired candidates:
 		<crate::DesiredCandidates<Test>>::put(1);
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(4)
+		));
 
 		// but no more
 		assert_noop!(
@@ -146,7 +151,9 @@
 	new_test_ext().execute_with(|| {
 		// reset desired candidates:
 		<crate::DesiredCandidates<Test>>::put(1);
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(4)
+		));
 
 		// can not remove too few
 		assert_noop!(
@@ -184,8 +191,13 @@
 fn cannot_register_dupe_candidate() {
 	new_test_ext().execute_with(|| {
 		// can add 3 as candidate
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
-		let addition = CandidateInfo { who: 3, deposit: 10 };
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(3)
+		));
+		let addition = CandidateInfo {
+			who: 3,
+			deposit: 10,
+		};
 		assert_eq!(CollatorSelection::candidates(), vec![addition]);
 		assert_eq!(CollatorSelection::last_authored_block(3), 10);
 		assert_eq!(Balances::free_balance(3), 90);
@@ -205,7 +217,9 @@
 		assert_eq!(Balances::free_balance(&33), 0);
 
 		// works
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(3)
+		));
 
 		// poor
 		assert_noop!(
@@ -228,8 +242,12 @@
 		assert_eq!(Balances::free_balance(&3), 100);
 		assert_eq!(Balances::free_balance(&4), 100);
 
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(3)
+		));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(4)
+		));
 
 		assert_eq!(Balances::free_balance(&3), 90);
 		assert_eq!(Balances::free_balance(&4), 90);
@@ -242,11 +260,15 @@
 fn leave_intent() {
 	new_test_ext().execute_with(|| {
 		// register a candidate.
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(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_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(5)
+		));
 		assert_eq!(Balances::free_balance(5), 90);
 
 		// cannot leave if not candidate.
@@ -270,11 +292,16 @@
 
 		// 4 is the default author.
 		assert_eq!(Balances::free_balance(4), 100);
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(4)
+		));
 		// triggers `note_author`
 		Authorship::on_initialize(1);
 
-		let collator = CandidateInfo { who: 4, deposit: 10 };
+		let collator = CandidateInfo {
+			who: 4,
+			deposit: 10,
+		};
 
 		assert_eq!(CollatorSelection::candidates(), vec![collator]);
 		assert_eq!(CollatorSelection::last_authored_block(4), 0);
@@ -295,11 +322,16 @@
 		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)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(4)
+		));
 		// triggers `note_author`
 		Authorship::on_initialize(1);
 
-		let collator = CandidateInfo { who: 4, deposit: 10 };
+		let collator = CandidateInfo {
+			who: 4,
+			deposit: 10,
+		};
 
 		assert_eq!(CollatorSelection::candidates(), vec![collator]);
 		assert_eq!(CollatorSelection::last_authored_block(4), 0);
@@ -324,7 +356,9 @@
 		assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
 
 		// add a new collator
-		assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(3)
+		));
 
 		// session won't see this.
 		assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
@@ -351,8 +385,12 @@
 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)));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(3)
+		));
+		assert_ok!(CollatorSelection::register_as_candidate(
+			RuntimeOrigin::signed(4)
+		));
 		initialize_to_block(10);
 		assert_eq!(CollatorSelection::candidates().len(), 2);
 		initialize_to_block(20);
@@ -361,7 +399,10 @@
 		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 = CandidateInfo { who: 4, deposit: 10 };
+		let collator = CandidateInfo {
+			who: 4,
+			deposit: 10,
+		};
 		assert_eq!(CollatorSelection::candidates(), vec![collator]);
 		assert_eq!(CollatorSelection::last_authored_block(4), 20);
 		initialize_to_block(30);
@@ -376,8 +417,12 @@
 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)));
+		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);
@@ -386,7 +431,10 @@
 		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 = CandidateInfo { who: 5, deposit: 10 };
+		let collator = CandidateInfo {
+			who: 5,
+			deposit: 10,
+		};
 		assert_eq!(CollatorSelection::candidates(), vec![collator]);
 		assert_eq!(CollatorSelection::last_authored_block(4), 20);
 		initialize_to_block(30);
@@ -401,7 +449,9 @@
 #[should_panic = "duplicate invulnerables in genesis."]
 fn cannot_set_genesis_value_twice() {
 	sp_tracing::try_init_simple();
-	let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
+	let mut t = frame_system::GenesisConfig::default()
+		.build_storage::<Test>()
+		.unwrap();
 	let invulnerables = vec![1, 1];
 
 	let collator_selection = collator_selection::GenesisConfig::<Test> {
modifiedpallets/collator-selection/src/weights.rsdiffbeforeafterboth
--- a/pallets/collator-selection/src/weights.rs
+++ b/pallets/collator-selection/src/weights.rs
@@ -41,6 +41,7 @@
 };
 use sp_std::marker::PhantomData;
 
+// todo:collator re-generate weights
 // The weight info trait for `pallet_collator_selection`.
 pub trait WeightInfo {
 	fn set_invulnerables(_b: u32) -> Weight;
modifiedprimitives/common/src/constants.rsdiffbeforeafterboth
--- a/primitives/common/src/constants.rs
+++ b/primitives/common/src/constants.rs
@@ -46,6 +46,8 @@
 pub const EXISTENTIAL_DEPOSIT: u128 = 0;
 /// Amount of Balance reserved for candidate registration.
 pub const GENESIS_CANDIDACY_BOND: u128 = EXISTENTIAL_DEPOSIT;
+/// How long a periodic session lasts in blocks.
+pub const SESSION_LENGTH: BlockNumber = MINUTES;
 
 // Targeting 0.1 UNQ per transfer
 pub const WEIGHT_TO_FEE_COEFF: u32 = /*<weight2fee>*/175_199_920/*</weight2fee>*/;
modifiedruntime/common/config/pallets/collator_selection.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/collator_selection.rs
+++ b/runtime/common/config/pallets/collator_selection.rs
@@ -18,12 +18,13 @@
 use frame_system::EnsureRoot;
 use crate::{
 	AccountId, BlockNumber, Runtime, RuntimeEvent, Balances, Aura, Session, SessionKeys,
-	CollatorSelection,
+	CollatorSelection, config::pallets::TreasuryAccountId,
 };
+use sp_runtime::Perbill;
 use up_common::constants::*;
 
 parameter_types! {
-	pub const SessionPeriod: BlockNumber = HOURS;
+	pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
 	pub const SessionOffset: BlockNumber = 0;
 }
 
@@ -54,9 +55,10 @@
 
 parameter_types! {
 	pub const PotId: PalletId = PalletId(*b"PotStake");
-	pub const MaxCandidates: u32 = 1000;
-	pub const MinCandidates: u32 = 5;
-	pub const MaxInvulnerables: u32 = 100;
+	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 SlashRatio: Perbill = Perbill::from_percent(100);
 }
 
 impl pallet_collator_selection::Config for Runtime {
@@ -64,13 +66,13 @@
 	type Currency = Balances;
 	// We allow root only to execute privileged collator selection operations.
 	type UpdateOrigin = EnsureRoot<AccountId>;
+	type TreasuryAccountId = TreasuryAccountId;
 	type PotId = PotId;
 	type MaxCandidates = MaxCandidates;
 	type MinCandidates = MinCandidates;
 	type MaxInvulnerables = MaxInvulnerables;
 	// todo:collator kick threshold should be in storage and configured only by root -- or rather UpdateOrigin
-	// Should be a multiple of session or things will get inconsistent.
-	type KickThreshold = SessionPeriod;
+	type SlashRatio = SlashRatio;
 	type ValidatorId = <Self as frame_system::Config>::AccountId;
 	type ValidatorIdOf = pallet_collator_selection::IdentityCollator;
 	type ValidatorRegistration = Session;
modifiedruntime/common/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/runtime_apis.rs
+++ b/runtime/common/runtime_apis.rs
@@ -703,6 +703,10 @@
                     #[cfg(feature = "rmrk")]
                     list_benchmark!(list, extra, pallet_proxy_rmrk_equip, RmrkEquip);
 
+                    // todo:collator check benchmarks
+                    #[cfg(feature = "collator-selection")]
+                    list_benchmark!(list, extra, pallet_collator_selection, CollatorSelection);
+
                     #[cfg(feature = "foreign-assets")]
                     list_benchmark!(list, extra, pallet_foreign_assets, ForeignAssets);
 
@@ -766,6 +770,10 @@
                     #[cfg(feature = "rmrk")]
                     add_benchmark!(params, batches, pallet_proxy_rmrk_equip, RmrkEquip);
 
+                    // todo:collator check benchmarks
+                    #[cfg(feature = "collator-selection")]
+                    add_benchmark!(params, batches, pallet_collator_selection, CollatorSelection);
+
                     #[cfg(feature = "foreign-assets")]
                     add_benchmark!(params, batches, pallet_foreign_assets, ForeignAssets);
 
modifiedtests/src/collatorSelection.test.tsdiffbeforeafterboth
--- a/tests/src/collatorSelection.test.ts
+++ b/tests/src/collatorSelection.test.ts
@@ -17,90 +17,256 @@
 import {IKeyringPair} from '@polkadot/types/types';
 import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from './util';
 
-// todo Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).
-describe('Integration Test: Dynamic shuffling of collators', () => {
+async function resetInvulnerables() {
+  await usingPlaygrounds(async (helper, privateKey) => {
+    const superuser = await privateKey('//Alice');
+    const alice = await privateKey('//Alice');
+    const bob = await privateKey('//Bob');
+    const invulnerables = await helper.collatorSelection.getInvulnerables();
+    if (!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {
+      console.warn('Alice and Bob are not the invulnerables! Reinstating them back. ' 
+        + 'Current invulnerables\' size: ' + invulnerables.length);
+      
+      let nonce = await helper.chain.getNonce(alice.address);
+      await Promise.all([
+        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),
+        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),
+      ]);
+
+      nonce = await helper.chain.getNonce(alice.address);
+      await Promise.all(invulnerables.map((invulnerable: any) => {
+        if (invulnerable == alice.address || invulnerable == bob.address) return new Promise<void>(res => res());
+        return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});
+      }));
+    }
+  });
+}
+
+// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).
+// + 18 tests: 5 (1+4) on session change
+describe('Integration Test: Collator Selection', () => {
   let superuser: IKeyringPair;
 
   // These are the default invulnerables, and should return to be invulnerables after this suite.
-  let aliceAddress: string;
-  let bobAddress: string;
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
 
   let charlie: IKeyringPair;
   let dave: IKeyringPair;
   //let eve: IKeyringPair;
 
-  before(async function() {
+  before(async function() {  
     await usingPlaygrounds(async (helper, privateKey) => {
       requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);
 
+      //todo:collator
       //const donor = await privateKey({filename: __filename});
       //[charlie, dave] = await helper.arrange.createAccounts([100n, 100n], donor);
+      alice = await privateKey('//Alice');
+      bob = await privateKey('//Bob');
       charlie = await privateKey('//Charlie');
       dave = await privateKey('//Dave');
 
       superuser = await privateKey('//Alice');
-      aliceAddress = (await privateKey('//Alice')).address;
-      bobAddress = (await privateKey('//Bob')).address;
+    });
+  });
+
+  describe('Dynamic shuffling of collators', () => {
+    before(async function() {  
+      await usingPlaygrounds(async (helper) => {
+        expect((await helper.collatorSelection.setOwnKeys(charlie))
+          .status.toLowerCase()).to.be.equal('success');
+        expect((await helper.collatorSelection.setOwnKeys(dave))
+          .status.toLowerCase()).to.be.equal('success');
+  
+        // todo:collator check necessity + add RPC for invulnerables / just improve in general
+        // validators = await helper.callRpc('api.query.session.validators');
+        const invulnerables = await helper.callRpc('api.query.collatorSelection.invulnerables');
+        if (!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {
+          console.warn('Alice and Bob are not the invulnerables! Reinstating them back. ' 
+            + 'Current invulnerables\' size: ' + invulnerables.length);
+          
+          await Promise.all([
+            helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: 0}),
+            helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: 1}),
+          ]);
+  
+          let nonce = 0;
+          await Promise.all(invulnerables.map((invulnerable: any) => {
+            if (invulnerable == alice.address || invulnerable == bob.address) return new Promise((res) => res);
+            return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});
+          }));
+        }
+      });
+    });
+  
+    itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {
+      await expect(Promise.all([
+        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [charlie.address], true, {nonce: 0}),
+        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [dave.address], true, {nonce: 1}),
+      ])).to.be.fulfilled;
+  
+      await expect(Promise.all([
+        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alice.address], true, {nonce: 0}),
+        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [bob.address], true, {nonce: 1}),
+      ])).to.be.fulfilled;
+  
+      const newInvulnerables = await helper.callRpc('api.query.collatorSelection.invulnerables');
+      expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+  
+      const expectedSessionIndex = (await helper.callRpc('api.query.session.currentIndex')).toNumber() + 2;
+      let currentSessionIndex = -1;
+      console.log('Waiting for the session after the next.' 
+        + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
+  
+      while (currentSessionIndex < expectedSessionIndex) {
+        // eslint-disable-next-line no-async-promise-executor
+        currentSessionIndex = await expect(helper.wait.withTimeout(new Promise(async (resolve) => {
+          //todo:collator
+          console.log('starting wait...');
+          console.time('ein');
+          await helper.wait.newBlocks(1);
+          console.timeLog('ein');
+          const res = (await helper.callRpc('api.query.session.currentIndex')).toNumber();
+          console.timeEnd('ein');
+          resolve(res);
+        }), 24000, 'The chain has stopped producing blocks!')).to.be.fulfilled;
+      }
+  
+      const newValidators = await helper.callRpc('api.query.session.validators');
+      expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+  
+      const lastBlockNumber = await helper.chain.getLatestBlockNumber();
+      await helper.wait.newBlocks(1);
+      const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber();
+      const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber();
+      expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true;
+    });
+  
+    // todo:collator keyless invulnerables? will hang, so, a breaking test, eh
+    // register candidate without sudos and the like
+  
+    after(async () => {
+      await usingPlaygrounds(async (helper) => {
+        if (helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;
+
+        await Promise.all([
+          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: 0}),
+          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: 1}),
+        ]);
+  
+        await Promise.all([
+          await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [charlie.address], true, {nonce: 0}),
+          await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [dave.address], true, {nonce: 1}),
+        ]);
+      });
+    });
+  });
+
+  // todo:collator make sure that there is enough session time for a set of tests
+  // 28 non-functioning collators, teehee.
+
+  describe('Addition and removal of invulnerables', () => {
+    before(async function() {
+      await resetInvulnerables();
+    });
 
-      expect((await helper.executeExtrinsic(charlie, 'api.tx.session.setKeys', [
-        '0x' + Buffer.from(charlie.addressRaw).toString('hex'),
-        '0x0',
-      ])).status.toLowerCase()).to.be.equal('success');
+    describe('Positive', () => {
+      itSub('Adds an invulnerable', async ({helper}) => {
+        const [account] = await helper.arrange.createAccounts([10n], superuser);
+        const invulnerables = await helper.collatorSelection.getInvulnerables();
 
-      expect((await helper.executeExtrinsic(dave, 'api.tx.session.setKeys', [
-        '0x' + Buffer.from(dave.addressRaw).toString('hex'),
-        '0x0',
-      ])).status.toLowerCase()).to.be.equal('success');
+        await helper.collatorSelection.setOwnKeys(account);
+        await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);
+        
+        const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+        expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);
+      });
 
-      const validators = await helper.callRpc('api.query.session.validators');
-      expect(validators).to.not.contain(charlie.address).and.not.contain(dave.address);
+      itSub('Removes an invulnerable', async ({helper}) => {
+        const invulnerables = await helper.collatorSelection.getInvulnerables();
+        const lastInvulnerable = invulnerables.pop();
+
+        await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);
+        const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+        // invulnerables had its last element removed, so they should be equal
+        expect(newInvulnerables).to.have.all.members(invulnerables);
+      });
     });
-  });
 
-  itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {
+    describe('Negative', () => {
+      itSub('Does not duplicate an invulnerable', async ({helper}) => {
+        const invulnerables = await helper.collatorSelection.getInvulnerables();
+        // adding an already invulnerable should not fail, but should not duplicate it either
+        await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))
+          .to.be.fulfilled;
+        const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+        expect(newInvulnerables).to.have.all.members(invulnerables);
+      });
+
+      itSub('Cannot allow invulnerables to be empty', async ({helper}) => {
+        const invulnerables = await helper.collatorSelection.getInvulnerables();
+        const lastInvulnerable = invulnerables.pop();
+
+        let nonce = await helper.chain.getNonce(superuser.address);
+        await Promise.all(invulnerables.map((i: any) => 
+          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));
 
-    const tx = helper.constructApiCall('api.tx.collatorSelection.setInvulnerables', [[
-      charlie.address,
-      dave.address,
-    ]]);
-    await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [tx])).to.be.fulfilled;
+        await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))
+          .to.be.rejected;//todo:collator With(/collatorSelection.TooFewInvulnerables/);
+
+        const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+        expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);
+        
+        // restore the invulnerables to the previous state
+        nonce = await helper.chain.getNonce(superuser.address);
+        await Promise.all(invulnerables.map((i: any) => 
+          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));
+      });
+
+      itSub('Cannot have too many invulnerables', async ({helper}) => {
+        const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;
+        const invulnerablesUntilLimit = 30 - invulnerablesLength;
+        const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);
+        const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);
 
-    const newInvulnerables = await helper.callRpc('api.query.collatorSelection.invulnerables');
-    expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+        await Promise.all(newInvulnerables.map((i: IKeyringPair) => 
+          helper.collatorSelection.setOwnKeys(i)));
+        await helper.collatorSelection.setOwnKeys(lastInvulnerable);
 
-    const expectedSessionIndex = (await helper.callRpc('api.query.session.currentIndex')).toNumber() + 2;
-    let currentSessionIndex = -1;
-    console.log('Waiting for the session after the next.' 
-      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
+        let nonce = await helper.chain.getNonce(superuser.address);
+        await Promise.all(newInvulnerables.map((i: IKeyringPair) => 
+          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));
 
-    while (currentSessionIndex < expectedSessionIndex) {
-      // eslint-disable-next-line no-async-promise-executor
-      currentSessionIndex = await expect(helper.wait.withTimeout(new Promise(async (resolve) => {
-        await helper.wait.newBlocks(1);
-        const res = (await helper.callRpc('api.query.session.currentIndex')).toNumber();
-        resolve(res);
-      }), 24000, 'The chain has stopped producing blocks!')).to.be.fulfilled;
-    }
+        await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))
+          .to.be.rejected; // todo:collator With(/collatorSelection.TooManyInvulnerables/);
+        
+        // restore the invulnerables to the previous state
+        nonce = await helper.chain.getNonce(superuser.address);
+        await Promise.all(newInvulnerables.map((i: IKeyringPair) => 
+          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));
+      });
 
-    const newValidators = await helper.callRpc('api.query.session.validators');
-    expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+      itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {
+        const [account] = await helper.arrange.createAccounts([10n], bob);
+        const invulnerables = await helper.collatorSelection.getInvulnerables();
 
-    const lastBlockNumber = await helper.chain.getLatestBlockNumber();
-    await helper.wait.newBlocks(1);
-    const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber();
-    const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber();
-    expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true;
-  });
+        await helper.collatorSelection.setOwnKeys(account);
+        await expect(helper.collatorSelection.addInvulnerable(bob, account.address))
+          .to.be.rejectedWith(/BadOrigin/);
 
-  after(async () => {
-    await usingPlaygrounds(async (helper) => {
-      if (helper.fetchMissingPalletNames([Pallets.AppPromotion]).length != 0) return;
+        const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+        expect(newInvulnerables).to.be.members(invulnerables);
+      });
 
-      const tx = helper.constructApiCall('api.tx.collatorSelection.setInvulnerables', [[
-        aliceAddress,
-        bobAddress,
-      ]]);
-      await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [tx])).to.be.fulfilled;
+      itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {
+        const invulnerables = await helper.collatorSelection.getInvulnerables();
+        await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))
+          .to.be.rejectedWith(/BadOrigin/);
+        expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);
+      });
     });
+    
+    // todo:collator after
   });
-});
+});
\ No newline at end of file
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -11,6 +11,7 @@
 import {IKeyringPair} from '@polkadot/types/types';
 import {hexToU8a} from '@polkadot/util/hex';
 import {u8aConcat} from '@polkadot/util/u8a';
+import {BN} from '@polkadot/util/bn';
 import {
   IApiListeners,
   IBlock,
@@ -2642,6 +2643,37 @@
   }
 }
 
+class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {
+  //todo:collator documentation
+  setKeys(signer: TSigner, key: string) {
+    return this.helper.executeExtrinsic(
+      signer,
+      'api.tx.session.setKeys', 
+      [
+        key,
+        '0x0',
+      ],
+      true,
+    );
+  }
+
+  setOwnKeys(signer: TSigner) {
+    return this.setKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));
+  }
+
+  addInvulnerable(signer: TSigner, address: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);
+  }
+
+  removeInvulnerable(signer: TSigner, address: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);
+  }
+
+  async getInvulnerables() {
+    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
+  }
+}
+
 class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {
   async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {
     await this.helper.executeExtrinsic(
@@ -2808,6 +2840,7 @@
   ft: FTGroup;
   staking: StakingGroup;
   scheduler: SchedulerGroup;
+  collatorSelection: CollatorSelectionGroup;
   foreignAssets: ForeignAssetsGroup;
   xcm: XcmGroup<UniqueHelper>;
   xTokens: XTokensGroup<UniqueHelper>;
@@ -2823,6 +2856,7 @@
     this.ft = new FTGroup(this);
     this.staking = new StakingGroup(this);
     this.scheduler = new SchedulerGroup(this);
+    this.collatorSelection = new CollatorSelectionGroup(this);
     this.foreignAssets = new ForeignAssetsGroup(this);
     this.xcm = new XcmGroup(this, 'polkadotXcm');
     this.xTokens = new XTokensGroup(this);
@@ -2988,19 +3022,32 @@
       super(...args);
     }
 
-    executeExtrinsic (
+    async executeExtrinsic(
       sender: IKeyringPair,
       extrinsic: string,
       params: any[],
       expectSuccess?: boolean,
+      options: Partial<SignerOptions>|null = null,
     ): Promise<ITransactionResult> {
       const call = this.constructApiCall(extrinsic, params);
-      return super.executeExtrinsic(
+      const result = await super.executeExtrinsic(
         sender,
         'api.tx.sudo.sudo',
         [call],
         expectSuccess,
+        options,
       );
+
+      if (result.status === 'Fail') return result;
+
+      const data = this.eventHelper.extractEvents(result.result.events).find(x => x.section == 'sudo')?.data[0];
+      if (data.err) {
+        const error = data.err.module;
+        // todo:collator
+        const metaError = super.getApi()?.registry.findMetaError({index: new BN(error.index), error: new BN(9)});
+        throw new Error(`${data.err.module.error} ${metaError.section}.${metaError.name}`);
+      }
+      return result;
     }
   };
 }