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
30// See the License for the specific language governing permissions and30// See the License for the specific language governing permissions and
31// limitations under the License.31// limitations under the License.
3232
33// todo:collator documentation
33//! Collator Selection pallet.34//! Collator Selection pallet.
34//!35//!
35//! A pallet to manage collators in a parachain.36//! A pallet to manage collators in a parachain.
109 };110 };
110 use frame_system::{pallet_prelude::*, Config as SystemConfig};111 use frame_system::{pallet_prelude::*, Config as SystemConfig};
111 use pallet_session::SessionManager;112 use pallet_session::SessionManager;
112 use sp_runtime::traits::Convert;113 use sp_runtime::{
114 Perbill,
115 traits::{One, Convert},
116 };
113 use sp_staking::SessionIndex;117 use sp_staking::SessionIndex;
114118
115 type BalanceOf<T> =119 type BalanceOf<T> =
136 /// Origin that can dictate updating parameters of this pallet.140 /// Origin that can dictate updating parameters of this pallet.
137 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;141 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
142
143 /// Account Identifier that holds the chain's treasury.
144 type TreasuryAccountId: Get<Self::AccountId>;
138145
139 /// Account Identifier from which the internal Pot is generated.146 /// Account Identifier from which the internal Pot is generated.
140 type PotId: Get<PalletId>;147 type PotId: Get<PalletId>;
152 /// Maximum number of invulnerables. This is enforced in code.159 /// Maximum number of invulnerables. This is enforced in code.
153 type MaxInvulnerables: Get<u32>;160 type MaxInvulnerables: Get<u32>;
154161
155 // Will be kicked if block is not produced in threshold.162 /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.
156 type KickThreshold: Get<Self::BlockNumber>;163 type SlashRatio: Get<Perbill>;
157164
158 /// A stable ID for a validator.165 /// A stable ID for a validator.
159 type ValidatorId: Member + Parameter;166 type ValidatorId: Member + Parameter;
200 ValueQuery,207 ValueQuery,
201 >;208 >;
209
210 /// Collator will be kicked if it does not produce a block within the threshold (does not apply to invulnerables).
211 ///
212 /// Should be a multiple of session or things will get inconsistent. todo:collator reword?
213 #[pallet::storage]
214 #[pallet::getter(fn kick_threshold)]
215 pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;
202216
203 /// Last block authored by collator.217 /// Last block authored by collator.
204 #[pallet::storage]218 #[pallet::storage]
224 pub struct GenesisConfig<T: Config> {238 pub struct GenesisConfig<T: Config> {
225 pub invulnerables: Vec<T::AccountId>,239 pub invulnerables: Vec<T::AccountId>,
226 pub candidacy_bond: BalanceOf<T>,240 pub candidacy_bond: BalanceOf<T>,
241 pub kick_threshold: T::BlockNumber,
227 pub desired_candidates: u32,242 pub desired_candidates: u32,
228 }243 }
229244
233 Self {248 Self {
234 invulnerables: Default::default(),249 invulnerables: Default::default(),
235 candidacy_bond: Default::default(),250 candidacy_bond: Default::default(),
251 kick_threshold: T::BlockNumber::one(),
236 desired_candidates: Default::default(),252 desired_candidates: Default::default(),
237 }253 }
238 }254 }
258276
259 <DesiredCandidates<T>>::put(&self.desired_candidates);277 <DesiredCandidates<T>>::put(&self.desired_candidates);
260 <CandidacyBond<T>>::put(&self.candidacy_bond);278 <CandidacyBond<T>>::put(&self.candidacy_bond);
279 <KickThreshold<T>>::put(&self.kick_threshold);
261 <Invulnerables<T>>::put(bounded_invulnerables);280 <Invulnerables<T>>::put(bounded_invulnerables);
262 }281 }
263 }282 }
264283
265 #[pallet::event]284 #[pallet::event]
266 #[pallet::generate_deposit(pub(super) fn deposit_event)]285 #[pallet::generate_deposit(pub(super) fn deposit_event)]
267 pub enum Event<T: Config> {286 pub enum Event<T: Config> {
268 NewInvulnerables { invulnerables: Vec<T::AccountId> },
269 NewDesiredCandidates { desired_candidates: u32 },287 NewDesiredCandidates {
288 desired_candidates: u32,
289 },
270 NewCandidacyBond { bond_amount: BalanceOf<T> },290 NewCandidacyBond {
291 bond_amount: BalanceOf<T>,
292 },
293 NewKickThreshold {
294 length_in_blocks: T::BlockNumber,
295 },
296 InvulnerableAdded {
297 invulnerable: T::AccountId,
298 },
299 InvulnerableRemoved {
300 invulnerable: T::AccountId,
301 },
271 CandidateAdded { account_id: T::AccountId, deposit: BalanceOf<T> },302 CandidateAdded {
303 account_id: T::AccountId,
304 deposit: BalanceOf<T>,
305 },
272 CandidateRemoved { account_id: T::AccountId },306 CandidateRemoved {
307 account_id: T::AccountId,
308 deposit_returned: BalanceOf<T>,
309 },
273 }310 }
274311
289 NotCandidate,326 NotCandidate,
290 /// Too many invulnerables327 /// Too many invulnerables
291 TooManyInvulnerables,328 TooManyInvulnerables,
329 /// Too few invulnerables
330 TooFewInvulnerables,
292 /// User is already an Invulnerable331 /// User is already an Invulnerable
293 AlreadyInvulnerable,332 AlreadyInvulnerable,
333 /// User is not an Invulnerable
334 NotInvulnerable,
294 /// Account has no associated validator ID335 /// Account has no associated validator ID
295 NoAssociatedValidatorId,336 NoAssociatedValidatorId,
296 /// Validator ID is not yet registered337 /// Validator ID is not yet registered
302343
303 #[pallet::call]344 #[pallet::call]
304 impl<T: Config> Pallet<T> {345 impl<T: Config> Pallet<T> {
305 /// Set the list of invulnerable (fixed) collators.346 /// Add a collator to the list of invulnerable (fixed) collators.
306 #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]347 #[pallet::weight(T::WeightInfo::set_invulnerables(1 as u32))] // todo:collator weight
307 pub fn set_invulnerables(348 pub fn add_invulnerable(
308 origin: OriginFor<T>,349 origin: OriginFor<T>,
309 new: Vec<T::AccountId>,350 new: T::AccountId,
310 ) -> DispatchResultWithPostInfo {351 ) -> DispatchResultWithPostInfo {
311 T::UpdateOrigin::ensure_origin(origin)?;352 T::UpdateOrigin::ensure_origin(origin)?;
312 let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)
313 .map_err(|_| Error::<T>::TooManyInvulnerables)?;
314353
315 // check if the invulnerables have associated validator keys before they are set354 // check if the new invulnerable has associated validator keys before it is added
316 for account_id in bounded_invulnerables.iter() {
317 let validator_key = T::ValidatorIdOf::convert(account_id.clone())355 let validator_key = T::ValidatorIdOf::convert(new.clone())
318 .ok_or(Error::<T>::NoAssociatedValidatorId)?;356 .ok_or(Error::<T>::NoAssociatedValidatorId)?;
319 ensure!(357 ensure!(
320 T::ValidatorRegistration::is_registered(&validator_key),358 T::ValidatorRegistration::is_registered(&validator_key),
321 Error::<T>::ValidatorNotRegistered359 Error::<T>::ValidatorNotRegistered
322 );360 );
323 }361 // ensure!(!Self::invulnerables().contains(&new), Error::<T>::AlreadyInvulnerable);
362 if Self::invulnerables().contains(&new) {
363 return Ok(().into());
364 }
324365
325 <Invulnerables<T>>::put(&bounded_invulnerables);366 <Invulnerables<T>>::try_append(new.clone())
367 .map_err(|_| Error::<T>::TooManyInvulnerables)?;
326 Self::deposit_event(Event::NewInvulnerables {368 Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });
327 invulnerables: bounded_invulnerables.to_vec(),
328 });
329 Ok(().into())369 Ok(().into())
330 }370 }
371
372 /// Remove a collator from the list of invulnerable (fixed) collators.
373 #[pallet::weight(T::WeightInfo::set_invulnerables(1))] // todo:collator weight
374 pub fn remove_invulnerable(
375 origin: OriginFor<T>,
376 who: T::AccountId,
377 ) -> DispatchResultWithPostInfo {
378 T::UpdateOrigin::ensure_origin(origin)?;
379
380 // let index = Self::invulnerables().into_iter().position(|r| r == who).ok_or(Error::<T>::NotInvulnerable)?;
381 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
382 if invulnerables.len() <= 1 {
383 return Err(Error::<T>::TooFewInvulnerables.into());
384 }
385
386 let index = invulnerables
387 .into_iter()
388 .position(|r| *r == who)
389 .ok_or(Error::<T>::NotInvulnerable)?;
390 invulnerables.remove(index);
391 Ok(())
392 })?;
393 /*let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)
394 .map_err(|_| Error::<T>::TooManyInvulnerables)?;
395
396 <Invulnerables<T>>::put(&bounded_invulnerables);*/
397 Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });
398 Ok(().into())
399 }
331400
332 /// Set the ideal number of collators (not including the invulnerables).401 /// 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.402 /// If lowering this number, then the number of running collators could be higher than this figure.
344 }413 }
345 <DesiredCandidates<T>>::put(&max);414 <DesiredCandidates<T>>::put(&max);
346 Self::deposit_event(Event::NewDesiredCandidates { desired_candidates: max });415 Self::deposit_event(Event::NewDesiredCandidates {
416 desired_candidates: max,
417 });
347 Ok(().into())418 Ok(().into())
348 }419 }
359 Ok(().into())430 Ok(().into())
360 }431 }
432
433 /// Set the length of the kick threshold.
434 /// Note that if the length is not a multiple of the session period, it might get inconsistent.
435 #[pallet::weight(T::WeightInfo::set_candidacy_bond())] // todo:collator weight
436 pub fn set_kick_threshold(
437 origin: OriginFor<T>,
438 kick_threshold: T::BlockNumber,
439 ) -> DispatchResultWithPostInfo {
440 T::UpdateOrigin::ensure_origin(origin)?;
441 // todo:collator insert something to guarantee consistency?
442 <KickThreshold<T>>::put(kick_threshold);
443 Self::deposit_event(Event::NewKickThreshold {
444 length_in_blocks: kick_threshold,
445 });
446 Ok(().into())
447 }
361448
362 /// Register this account as a collator candidate. The account must (a) already have449 /// Register this account as a collator candidate. The account must (a) already have
363 /// registered session keys and (b) be able to reserve the `CandidacyBond`.450 /// registered session keys and (b) be able to reserve the `CandidacyBond`.
460 (length as u32) < Self::desired_candidates(),
461 Error::<T>::TooManyCandidates
462 );
463 // todo:collator really need it?
373 ensure!(!Self::invulnerables().contains(&who), Error::<T>::AlreadyInvulnerable);464 ensure!(
465 !Self::invulnerables().contains(&who),
466 Error::<T>::AlreadyInvulnerable
383 // First authored block is current block plus kick threshold to handle session delay477 // First authored block is current block plus kick threshold to handle session delay
384 let incoming = CandidateInfo { who: who.clone(), deposit };478 let incoming = CandidateInfo {
479 who: who.clone(),
480 deposit,
481 };
385482
386 let current_count =483 let current_count =
491 .map_err(|_| Error::<T>::TooManyCandidates)?;
393 <LastAuthoredBlock<T>>::insert(492 <LastAuthoredBlock<T>>::insert(
394 who.clone(),493 who.clone(),
395 frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),494 frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),
396 );495 );
397 Ok(candidates.len())496 Ok(candidates.len())
398 }497 }
399 })?;498 })?;
400499
401 Self::deposit_event(Event::CandidateAdded { account_id: who, deposit });500 Self::deposit_event(Event::CandidateAdded {
501 account_id: who,
502 deposit,
503 });
402 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())504 Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())
403 }505 }
411 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]513 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]
412 pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {514 pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
413 let who = ensure_signed(origin)?;515 let who = ensure_signed(origin)?;
516 // todo:collator invulnerables and candidates should count against min candidates together
414 ensure!(517 ensure!(
415 Self::candidates().len() as u32 > T::MinCandidates::get(),518 Self::candidates().len() as u32 > T::MinCandidates::get(),
416 Error::<T>::TooFewCandidates519 Error::<T>::TooFewCandidates
417 );520 );
418 let current_count = Self::try_remove_candidate(&who)?;521 let current_count = Self::try_remove_candidate(&who, false)?;
419522
420 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())523 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
421 }524 }
427 T::PotId::get().into_account_truncating()530 T::PotId::get().into_account_truncating()
428 }531 }
429532
430 /// Removes a candidate if they exist and sends them back their deposit533 /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.
431 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {534 fn try_remove_candidate(
535 who: &T::AccountId,
536 should_slash: bool,
537 ) -> Result<usize, DispatchError> {
538 let mut deposit_returned = BalanceOf::<T>::default();
432 let current_count =539 let current_count =
433 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {540 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {
434 let index = candidates541 let index = candidates
435 .iter()542 .iter()
436 .position(|candidate| candidate.who == *who)543 .position(|candidate| candidate.who == *who)
437 .ok_or(Error::<T>::NotCandidate)?;544 .ok_or(Error::<T>::NotCandidate)?;
438 let candidate = candidates.remove(index);545 let candidate = candidates.remove(index);
546 let deposit = candidate.deposit;
547
548 if should_slash {
549 let slashed = T::SlashRatio::get() * deposit;
550 let remaining = deposit - slashed;
551
552 let (imbalance, _) = T::Currency::slash_reserved(who, slashed);
553 //T::Currency::unreserve(who, remaining);
554 deposit_returned = remaining;
555
556 T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);
557
558 // Self::deposit_event(Event::CandidateSlashed(who.clone()));
559 } else {
560 //T::Currency::unreserve(who, deposit);
561 deposit_returned = deposit;
562 }
563
439 T::Currency::unreserve(who, candidate.deposit);564 T::Currency::unreserve(who, deposit_returned);
565 // candidates.remove(index);
440 <LastAuthoredBlock<T>>::remove(who.clone());566 <LastAuthoredBlock<T>>::remove(who.clone());
441 Ok(candidates.len())567 Ok(candidates.len())
442 })?;568 })?;
443 Self::deposit_event(Event::CandidateRemoved { account_id: who.clone() });569 Self::deposit_event(Event::CandidateRemoved {
570 account_id: who.clone(),
571 deposit_returned,
572 });
444 Ok(current_count)573 Ok(current_count)
445 }574 }
456 }585 }
457586
458 /// Kicks out candidates that did not produce a block in the kick threshold587 /// Kicks out candidates that did not produce a block in the kick threshold
459 /// and refund their deposits.588 /// and **confiscates** their deposits to the treasury.
460 pub fn kick_stale_candidates(589 pub fn kick_stale_candidates(
461 candidates: BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,590 candidates: BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,
462 ) -> BoundedVec<T::AccountId, T::MaxCandidates> {591 ) -> BoundedVec<T::AccountId, T::MaxCandidates> {
463 let now = frame_system::Pallet::<T>::block_number();592 let now = frame_system::Pallet::<T>::block_number();
464 let kick_threshold = T::KickThreshold::get();593 let kick_threshold = Self::kick_threshold();
465 candidates594 candidates
466 .into_iter()595 .into_iter()
467 .filter_map(|c| {596 .filter_map(|c| {
472 {601 {
473 Some(c.who)602 Some(c.who)
474 } else {603 } else {
475 let outcome = Self::try_remove_candidate(&c.who);604 let outcome = Self::try_remove_candidate(&c.who, true);
476 if let Err(why) = outcome {605 if let Err(why) = outcome {
477 log::warn!("Failed to remove candidate {:?}", why);606 log::warn!("Failed to remove candidate {:?}", why);
478 debug_assert!(false, "failed to remove candidate {:?}", why);607 debug_assert!(false, "failed to remove candidate {:?}", why);
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;
     }
   };
 }