From 496d044f6481027f3a6f822770429f3e41d8d246 Mon Sep 17 00:00:00 2001
From: tmpolaczyk <44604217+tmpolaczyk@users.noreply.github.com>
Date: Mon, 22 Jan 2024 15:41:39 +0100
Subject: [PATCH] Rewrite collator assignment to support parathreads (#385)
---
pallets/collator-assignment/src/assignment.rs | 436 +++++++++++++
pallets/collator-assignment/src/lib.rs | 199 ++----
pallets/collator-assignment/src/mock.rs | 32 +-
pallets/collator-assignment/src/tests.rs | 593 ++++++++++++------
.../src/tests/assign_full.rs | 90 +++
.../src/tests/prioritize_invulnerables.rs | 186 ++++++
.../src/tests/select_chains.rs | 176 ++++++
pallets/configuration/src/lib.rs | 18 +-
pallets/registrar/src/lib.rs | 5 +
primitives/traits/src/lib.rs | 2 +
runtime/dancebox/tests/integration_test.rs | 7 +-
runtime/flashbox/tests/integration_test.rs | 7 +-
test/scripts/build-spec-warp-sync.sh | 2 +-
test/scripts/build-spec.sh | 2 +-
.../test_remove_session_key_staking.ts | 21 +-
.../staking/test_staking_rewards_balanced.ts | 14 +-
.../test_staking_rewards_non_balanced.ts | 14 +-
17 files changed, 1433 insertions(+), 371 deletions(-)
create mode 100644 pallets/collator-assignment/src/assignment.rs
create mode 100644 pallets/collator-assignment/src/tests/assign_full.rs
create mode 100644 pallets/collator-assignment/src/tests/prioritize_invulnerables.rs
create mode 100644 pallets/collator-assignment/src/tests/select_chains.rs
diff --git a/pallets/collator-assignment/src/assignment.rs b/pallets/collator-assignment/src/assignment.rs
new file mode 100644
index 000000000..fb17885b6
--- /dev/null
+++ b/pallets/collator-assignment/src/assignment.rs
@@ -0,0 +1,436 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+use {
+ dp_collator_assignment::AssignedCollators,
+ sp_std::{
+ cmp,
+ collections::{btree_map::BTreeMap, btree_set::BTreeSet},
+ marker::PhantomData,
+ mem, vec,
+ vec::Vec,
+ },
+ tp_traits::{ParaId, RemoveInvulnerables as RemoveInvulnerablesT},
+};
+
+/// Helper methods to implement collator assignment algorithm
+pub struct Assignment(PhantomData);
+
+impl Assignment
+where
+ T: crate::Config,
+{
+ /// Recompute collator assignment from scratch. If the list of collators and the list of
+ /// container chains are shuffled, this returns a random assignment.
+ pub fn assign_collators_rotate_all(
+ collators: Vec,
+ orchestrator_chain: ChainNumCollators,
+ chains: Vec,
+ ) -> Result, AssignmentError> {
+ // This is just the "always_keep_old" algorithm but with an empty "old"
+ let old_assigned = Default::default();
+
+ Self::assign_collators_always_keep_old(collators, orchestrator_chain, chains, old_assigned)
+ }
+
+ /// Assign new collators to missing container_chains.
+ /// Old collators always have preference to remain on the same chain.
+ /// If there are no missing collators, nothing is changed.
+ ///
+ /// `chains` should be shuffled or at least rotated on every session to ensure
+ /// a fair distribution, because the order of that list affects container chain priority:
+ /// the first chain on that list will be the first one to get new collators.
+ pub fn assign_collators_always_keep_old(
+ collators: Vec,
+ orchestrator_chain: ChainNumCollators,
+ mut chains: Vec,
+ mut old_assigned: AssignedCollators,
+ ) -> Result, AssignmentError> {
+ if collators.is_empty() {
+ return Err(AssignmentError::ZeroCollators);
+ }
+ // The rest of this function mostly treats orchestrator chain as another container chain, so move it into
+ // `old_assigned.container_chains`
+ let old_orchestrator_assigned = mem::take(&mut old_assigned.orchestrator_chain);
+ old_assigned
+ .container_chains
+ .insert(orchestrator_chain.para_id, old_orchestrator_assigned);
+ let mut old_assigned = old_assigned.container_chains;
+ // Orchestrator chain must be the first one in the list because it always has priority
+ chains.insert(0, orchestrator_chain);
+ let all_para_ids: Vec = chains.iter().map(|cc| cc.para_id).collect();
+ let collators_set = BTreeSet::from_iter(collators.iter().cloned());
+ let chains_with_collators =
+ Self::select_chains_with_collators(collators.len() as u32, &chains);
+ let chains_with_collators_set: BTreeSet = chains_with_collators
+ .iter()
+ .map(|(para_id, _num_collators)| *para_id)
+ .collect();
+ Self::retain_valid_old_assigned(
+ &mut old_assigned,
+ &chains_with_collators_set,
+ &collators_set,
+ );
+
+ // Ensure the first `min_orchestrator_collators` of orchestrator chain are invulnerables
+ Self::prioritize_invulnerables(&collators, orchestrator_chain, &mut old_assigned);
+
+ let new_assigned_chains =
+ Self::assign_full(collators, chains_with_collators, old_assigned)?;
+ let mut new_assigned = AssignedCollators::default();
+ new_assigned.container_chains = new_assigned_chains;
+
+ // Add container chains with 0 collators so that they are shown in UI
+ for para_id in all_para_ids {
+ new_assigned.container_chains.entry(para_id).or_default();
+ }
+
+ // The rest of this function mostly treats orchestrator chain as another container chain, remove it from
+ // container chains before returning the final assignment.
+ let orchestrator_assigned = new_assigned
+ .container_chains
+ .remove(&orchestrator_chain.para_id)
+ .unwrap();
+ // Sanity check to avoid bricking orchestrator chain
+ if orchestrator_assigned.is_empty() {
+ return Err(AssignmentError::EmptyOrchestrator);
+ }
+ new_assigned.orchestrator_chain = orchestrator_assigned;
+
+ Ok(new_assigned)
+ }
+
+ /// Select which container chains will be assigned collators and how many collators, but do not specify which
+ /// collator goes to which chain.
+ ///
+ /// Each chain has a min and max number of collators. If the number of collators is not enough to reach the min,
+ /// no collators are assigned to that chain.
+ ///
+ /// If the available number of collators is:
+ /// * lower than the min of the first chain: we assign all the collators to the first chain. This is the
+ /// orchestrator chain and we always want it to have collators.
+ /// * lower than the sum of all the min: we cannot assign collators to all the chains. So remove chains until
+ /// we can. The order is important, the first chains will be assigned collators and the last ones will not.
+ /// * lower than the sum of all the max: we can assign the min value to all the chains, and have some leftover.
+ /// We use the same order to decide where this extra collators will go, by filling the max of the first chain,
+ /// then the max of the second chain, and so on.
+ /// * greater than the sum of all the max: all the chains will be assigned their max number of collators.
+ ///
+ /// # Params
+ ///
+ /// The first item of `chains` should be the orchestrator chain, because it will be the first one to be assigned
+ /// collators.
+ ///
+ /// # Returns
+ ///
+ /// A list of `(para_id, num_collators)`.
+ pub fn select_chains_with_collators(
+ num_collators: u32,
+ chains: &[ChainNumCollators],
+ ) -> Vec<(ParaId, u32)> {
+ if chains.is_empty() {
+ // Avoid panic if chains is empty
+ return vec![];
+ }
+ // Let's count how many container chains we can support with the current number of collators
+ let mut available_collators = num_collators;
+ // Handle orchestrator chain in a special way, we always want to assign collators to it, even if we don't
+ // reach the min.
+ let min_orchestrator_collators = chains[0].min_collators;
+ available_collators = available_collators.saturating_sub(min_orchestrator_collators);
+
+ let mut container_chains_with_collators = vec![chains[0]];
+ // Skipping orchestrator chain because it was handled above
+ for cc in chains.iter().skip(1) {
+ if available_collators >= cc.min_collators {
+ available_collators -= cc.min_collators;
+ container_chains_with_collators.push(*cc);
+ } else if available_collators == 0 {
+ // Do not break if there are still some available collators. Even if they were not enough to reach the
+ // `min` of this chain, it is possible that one of the chains with less priority has a lower `min`, so
+ // that chain should be assigned collators.
+ break;
+ }
+ }
+
+ let mut required_collators_min = 0;
+ for cc in &container_chains_with_collators {
+ required_collators_min += cc.min_collators;
+ }
+
+ if num_collators < min_orchestrator_collators {
+ // Edge case: num collators less than min orchestrator collators: fill as much as we can
+ vec![(chains[0].para_id, num_collators)]
+ } else {
+ // After assigning the min to all the chains we have this remainder. The remainder will be assigned until
+ // all the chains reach the max value.
+ let mut required_collators_remainder = num_collators - required_collators_min;
+ let mut container_chains_variable = vec![];
+ for cc in &container_chains_with_collators {
+ // Each chain will have `min + extra` collators, where extra is capped so `min + extra <= max`.
+ let extra = cmp::min(
+ required_collators_remainder,
+ cc.max_collators.saturating_sub(cc.min_collators),
+ );
+ let num = cc.min_collators + extra;
+ required_collators_remainder -= extra;
+ container_chains_variable.push((cc.para_id, num));
+ }
+
+ container_chains_variable
+ }
+ }
+
+ /// Same as `prioritize_invulnerables` but return the invulnerables instead of inserting them into `old_assigned`.
+ ///
+ /// Mutates `old_assigned` by removing invulnerables from their old chain, even if they will later be assigned to
+ /// the same chain.
+ pub fn remove_invulnerables(
+ collators: &[T::AccountId],
+ orchestrator_chain: ChainNumCollators,
+ old_assigned: &mut BTreeMap>,
+ ) -> Vec {
+ // TODO: clean this up, maybe change remove_invulnerables trait into something more ergonomic
+ let min_orchestrator_collators = orchestrator_chain.min_collators as usize;
+ let invulnerables_already_assigned = T::RemoveInvulnerables::remove_invulnerables(
+ &mut old_assigned
+ .get(&orchestrator_chain.para_id)
+ .cloned()
+ .unwrap_or_default(),
+ min_orchestrator_collators,
+ );
+ let mut new_invulnerables = invulnerables_already_assigned;
+ if new_invulnerables.len() >= min_orchestrator_collators {
+ // We already had invulnerables, we will just move them to the front of the list if they weren't already
+ return new_invulnerables;
+ }
+
+ // Not enough invulnerables currently assigned, get rest from new_collators
+ let mut new_collators = collators.to_vec();
+ for (_id, cs) in old_assigned.iter() {
+ new_collators.retain(|c| !cs.contains(c));
+ }
+ let num_missing_invulnerables = min_orchestrator_collators - new_invulnerables.len();
+ let invulnerables_not_assigned = T::RemoveInvulnerables::remove_invulnerables(
+ &mut new_collators,
+ num_missing_invulnerables,
+ );
+ new_invulnerables.extend(invulnerables_not_assigned);
+
+ if new_invulnerables.len() >= min_orchestrator_collators {
+ // Got invulnerables from new_collators, and maybe some were already assigned
+ return new_invulnerables;
+ }
+
+ // Still not enough invulnerables, try to get an invulnerable that is currently assigned somewhere else
+ let num_missing_invulnerables = min_orchestrator_collators - new_invulnerables.len();
+ let mut collators = collators.to_vec();
+ let new_invulnerables_set = BTreeSet::from_iter(new_invulnerables.iter().cloned());
+ collators.retain(|c| {
+ // Remove collators already selected
+ !new_invulnerables_set.contains(c)
+ });
+ let invulnerables_assigned_elsewhere =
+ T::RemoveInvulnerables::remove_invulnerables(&mut collators, num_missing_invulnerables);
+
+ if invulnerables_assigned_elsewhere.is_empty() {
+ // If at this point we still do not have enough invulnerables, it means that there are no
+ // enough invulnerables, so no problem, but return the invulnerables
+ return new_invulnerables;
+ }
+
+ new_invulnerables.extend(invulnerables_assigned_elsewhere.iter().cloned());
+
+ // In this case we must delete the old assignment of the invulnerables
+ let reassigned_invulnerables_set = BTreeSet::from_iter(invulnerables_assigned_elsewhere);
+ // old_assigned.remove_collators_in_set
+ for (_id, cs) in old_assigned.iter_mut() {
+ cs.retain(|c| !reassigned_invulnerables_set.contains(c));
+ }
+
+ new_invulnerables
+ }
+
+ /// Ensure orchestrator chain has `min_orchestrator` invulnerables. If that's not possible, it tries to add as
+ /// many invulnerables as possible.
+ ///
+ /// Get invulnerables from:
+ /// * old_assigned in orchestrator
+ /// * new collators
+ /// * old_assigned elsewhere
+ ///
+ /// In that order.
+ ///
+ /// Mutates `old_assigned` because invulnerables will be inserted there, and if invulnerables were already
+ /// assigned to some other chain, they will be removed from that other chain as well.
+ ///
+ /// # Params
+ ///
+ /// * `old_assigned` must be a subset of `collators`
+ /// * `old_assigned` must not have duplicate collators.
+ ///
+ /// # Returns
+ ///
+ /// The number of invulnerables assigned to the orchestrator chain, capped to `min_collators`.
+ pub fn prioritize_invulnerables(
+ collators: &[T::AccountId],
+ orchestrator_chain: ChainNumCollators,
+ old_assigned: &mut BTreeMap>,
+ ) -> usize {
+ let new_invulnerables =
+ Self::remove_invulnerables(collators, orchestrator_chain, old_assigned);
+
+ if !new_invulnerables.is_empty() {
+ Self::insert_invulnerables(
+ old_assigned.entry(orchestrator_chain.para_id).or_default(),
+ &new_invulnerables,
+ );
+ }
+
+ new_invulnerables.len()
+ }
+
+ /// Assign collators assuming that the number of collators is greater than or equal to the required.
+ /// The order of both container chains and collators is important to ensure randomness when `old_assigned` is
+ /// empty.
+ ///
+ /// # Params
+ ///
+ /// * `old_assigned` does not need to be a subset of `collators`: collators are checked and removed.
+ /// * `old_assigned` does not need to be a subset of `chains`, unused para ids are removed. Collators
+ /// assigned to a para_id not present in `chains` may be reassigned to another para_id.
+ /// * `chains` `num_collators` can be 0. In that case an empty vec is returned for that para id.
+ /// * `old_assigned` must not have duplicate collators.
+ ///
+ /// # Returns
+ ///
+ /// The collator assigment, a map from `ParaId` to `Vec`.
+ ///
+ /// Or an error if the number of collators is not enough to fill all the chains, or if the required number
+ /// of collators overflows a `u32`.
+ pub fn assign_full(
+ collators: Vec,
+ chains: Vec<(ParaId, u32)>,
+ mut old_assigned: BTreeMap>,
+ ) -> Result>, AssignmentError> {
+ let mut required_collators = 0usize;
+ for (_para_id, num_collators) in chains.iter() {
+ let num_collators =
+ usize::try_from(*num_collators).map_err(|_| AssignmentError::NotEnoughCollators)?;
+ required_collators = required_collators
+ .checked_add(num_collators)
+ .ok_or(AssignmentError::NotEnoughCollators)?;
+ }
+
+ // This check is necessary to ensure priority: if the number of collators is less than required, it is
+ // possible that the chain with the least priority could be assigned collators (since they are in
+ // old_assigned), while some chains with higher priority might have no collators.
+ if collators.len() < required_collators {
+ return Err(AssignmentError::NotEnoughCollators);
+ }
+ // We checked that the sum of all `num_collators` fits in `usize`, so we can safely use `as usize`.
+
+ // Remove invalid collators and para ids from `old_assigned`
+ let para_ids_set =
+ BTreeSet::from_iter(chains.iter().map(|(para_id, _num_collators)| *para_id));
+ let collators_set = BTreeSet::from_iter(collators.iter().cloned());
+ Self::retain_valid_old_assigned(&mut old_assigned, ¶_ids_set, &collators_set);
+
+ // Truncate num collators to required
+ for (para_id, num_collators) in chains.iter() {
+ let entry = old_assigned.entry(*para_id).or_default();
+ entry.truncate(*num_collators as usize);
+ }
+
+ let assigned_collators: BTreeSet = old_assigned
+ .iter()
+ .flat_map(|(_para_id, para_collators)| para_collators.iter().cloned())
+ .collect();
+ let mut new_collators = collators.into_iter().filter(|x| {
+ // Keep collators not already assigned
+ !assigned_collators.contains(x)
+ });
+
+ // Fill missing collators
+ for (para_id, num_collators) in chains.iter() {
+ let cs = old_assigned.entry(*para_id).or_default();
+
+ while cs.len() < *num_collators as usize {
+ // This error should never happen because we checked that `collators.len() >= required_collators`
+ let nc = new_collators
+ .next()
+ .ok_or(AssignmentError::NotEnoughCollators)?;
+ cs.push(nc);
+ }
+ }
+
+ Ok(old_assigned)
+ }
+
+ /// Insert invulnerables ensuring that they are always the first in the list.
+ /// The order of both lists is preserved.
+ /// `assigned` may already contain the invulnerables, in that case they are only moved to the front.
+ ///
+ /// Invulnerables need to be the first of the list because we may truncate the list of collators if the number of
+ /// collators changes, and in that case we want invulnerables to stay assigned there.
+ pub fn insert_invulnerables(assigned: &mut Vec, invulnerables: &[T::AccountId]) {
+ assigned.retain(|item| !invulnerables.contains(item));
+
+ let mut new_assigned = invulnerables.to_vec();
+ new_assigned.extend(mem::take(assigned));
+
+ *assigned = new_assigned;
+ }
+
+ /// Removes invalid entries from `old_assigned`:
+ ///
+ /// * para ids not in `chains_with_collators`
+ /// * collators not in `collators`
+ pub fn retain_valid_old_assigned(
+ old_assigned: &mut BTreeMap>,
+ chains_with_collators: &BTreeSet,
+ collators: &BTreeSet,
+ ) {
+ // old_assigned.remove_container_chains_not_in_set
+ old_assigned.retain(|id, _cs| chains_with_collators.contains(id));
+ // old_assigned.remove_collators_not_in_set
+ for (_id, cs) in old_assigned.iter_mut() {
+ cs.retain(|c| collators.contains(c));
+ }
+ }
+}
+
+/// Errors than can happen during collator assignment
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum AssignmentError {
+ /// An empty list of collators was passed to `assign_collators_always_keep_old`
+ ZeroCollators,
+ /// The required number of collators for `assign_full` is greater than the provided number of collators.
+ /// Also includes possible overflows in number of collators.
+ NotEnoughCollators,
+ /// No collators were assigned to orchestrator chain
+ EmptyOrchestrator,
+}
+
+/// A `ParaId` and a range of collators that need to be assigned to it.
+/// This can be a container chain, a parathread, or the orchestrator chain.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub struct ChainNumCollators {
+ pub para_id: ParaId,
+ pub min_collators: u32,
+ // This will only be filled if all the other min have been reached
+ pub max_collators: u32,
+}
diff --git a/pallets/collator-assignment/src/lib.rs b/pallets/collator-assignment/src/lib.rs
index eb10e53de..5df502dec 100644
--- a/pallets/collator-assignment/src/lib.rs
+++ b/pallets/collator-assignment/src/lib.rs
@@ -43,7 +43,10 @@
pub use pallet::*;
use {
- crate::weights::WeightInfo,
+ crate::{
+ assignment::{Assignment, ChainNumCollators},
+ weights::WeightInfo,
+ },
dp_collator_assignment::AssignedCollators,
frame_support::pallet_prelude::*,
frame_system::pallet_prelude::BlockNumberFor,
@@ -60,6 +63,7 @@ use {
},
};
+mod assignment;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
@@ -164,6 +168,7 @@ pub mod pallet {
// We get the containerChains that we will have at the target session
let mut container_chain_ids =
T::ContainerChains::session_container_chains(target_session_index);
+ let mut parathreads = T::ContainerChains::session_parathreads(target_session_index);
let num_total_registered_paras = container_chain_ids.len() as u32;
// Remove the containerChains that do not have enough credits for block production
T::RemoveParaIdsWithNoCredits::remove_para_ids_with_no_credits(
@@ -176,11 +181,47 @@ pub mod pallet {
if random_seed != [0; 32] {
let mut rng: ChaCha20Rng = SeedableRng::from_seed(random_seed);
collators.shuffle(&mut rng);
+ // TODO: in the future, instead of shuffling the list of para ids, we need to use the priority fee to
+ // determine priority
container_chain_ids.shuffle(&mut rng);
+ parathreads.shuffle(&mut rng);
}
// We read current assigned collators
let old_assigned = Self::read_assigned_collators();
+ let orchestrator_chain = ChainNumCollators {
+ para_id: T::SelfParaId::get(),
+ min_collators: T::HostConfiguration::min_collators_for_orchestrator(
+ target_session_index,
+ ),
+ max_collators: T::HostConfiguration::max_collators_for_orchestrator(
+ target_session_index,
+ ),
+ };
+ // Initialize list of chains as `[container1, container2, parathread1, parathread2]`.
+ // The order means priority: the first chain in the list will be the first one to get assigned collators.
+ // Chains will not be assigned less than `min_collators`, except the orchestrator chain.
+ // First all chains will be assigned `min_collators`, and then the first one will be assigned up to `max`,
+ // then the second one, and so on.
+ let mut chains = vec![];
+ let collators_per_container =
+ T::HostConfiguration::collators_per_container(target_session_index);
+ for para_id in &container_chain_ids {
+ chains.push(ChainNumCollators {
+ para_id: *para_id,
+ min_collators: collators_per_container,
+ max_collators: collators_per_container,
+ });
+ }
+ let collators_per_parathread =
+ T::HostConfiguration::collators_per_parathread(target_session_index);
+ for para_id in ¶threads {
+ chains.push(ChainNumCollators {
+ para_id: *para_id,
+ min_collators: collators_per_parathread,
+ max_collators: collators_per_parathread,
+ });
+ }
// We assign new collators
// we use the config scheduled at the target_session_index
let new_assigned =
@@ -197,15 +238,10 @@ pub mod pallet {
target_session: target_session_index,
});
- Self::assign_collators_rotate_all(
+ Assignment::::assign_collators_rotate_all(
collators,
- &container_chain_ids,
- T::HostConfiguration::min_collators_for_orchestrator(target_session_index)
- as usize,
- T::HostConfiguration::max_collators_for_orchestrator(target_session_index)
- as usize,
- T::HostConfiguration::collators_per_container(target_session_index)
- as usize,
+ orchestrator_chain,
+ chains,
)
} else {
log::info!(
@@ -220,19 +256,26 @@ pub mod pallet {
target_session: target_session_index,
});
- Self::assign_collators_always_keep_old(
+ Assignment::::assign_collators_always_keep_old(
collators,
- &container_chain_ids,
- T::HostConfiguration::min_collators_for_orchestrator(target_session_index)
- as usize,
- T::HostConfiguration::max_collators_for_orchestrator(target_session_index)
- as usize,
- T::HostConfiguration::collators_per_container(target_session_index)
- as usize,
+ orchestrator_chain,
+ chains,
old_assigned.clone(),
)
};
+ let new_assigned = match new_assigned {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!(
+ "Error in collator assignment, will keep previous assignment. {:?}",
+ e
+ );
+
+ old_assigned.clone()
+ }
+ };
+
let mut pending = PendingCollatorContainerChain::::get();
let old_assigned_changed = old_assigned != new_assigned;
let mut pending_changed = false;
@@ -267,128 +310,6 @@ pub mod pallet {
}
}
- /// Recompute collator assignment from scratch. If the list of collators and the list of
- /// container chains are shuffled, this returns a random assignment.
- fn assign_collators_rotate_all(
- collators: Vec,
- container_chain_ids: &[ParaId],
- min_num_orchestrator_chain: usize,
- max_num_orchestrator_chain: usize,
- num_each_container_chain: usize,
- ) -> AssignedCollators {
- // This is just the "always_keep_old" algorithm but with an empty "old"
- let old_assigned = Default::default();
-
- Self::assign_collators_always_keep_old(
- collators,
- container_chain_ids,
- min_num_orchestrator_chain,
- max_num_orchestrator_chain,
- num_each_container_chain,
- old_assigned,
- )
- }
-
- /// Assign new collators to missing container_chains.
- /// Old collators always have preference to remain on the same chain.
- /// If there are no missing collators, nothing is changed.
- ///
- /// `container_chain_ids` should be shuffled or at least rotated on every session to ensure
- /// a fair distribution, because the order of that list affects container chain priority:
- /// the first container chain on that list will be the first one to get new collators.
- fn assign_collators_always_keep_old(
- collators: Vec,
- container_chain_ids: &[ParaId],
- min_num_orchestrator_chain: usize,
- max_num_orchestrator_chain: usize,
- num_each_container_chain: usize,
- old_assigned: AssignedCollators,
- ) -> AssignedCollators {
- // TODO: the performance of this function is sad, could be improved by having sets of
- // old_collators and new_collators instead of doing array.contains() every time.
- let mut new_assigned = old_assigned;
- new_assigned.remove_collators_not_in_list(&collators);
- new_assigned.remove_container_chains_not_in_list(container_chain_ids);
- let extra_orchestrator_collators =
- new_assigned.remove_orchestrator_chain_excess_collators(min_num_orchestrator_chain);
- // Only need to do this if the config params change
- new_assigned.remove_container_chain_excess_collators(num_each_container_chain);
-
- // Collators that are not present in old_assigned
- // This is used to keep track of which collators are old and which ones are new, to keep
- // the old collators on the same chain if possible.
- let mut new_collators = vec![];
- for c in collators {
- if !new_assigned.find_collator(&c) && !extra_orchestrator_collators.contains(&c) {
- new_collators.push(c);
- }
- }
-
- // Fill orchestrator chain collators up to min_num_orchestrator_chain
- // Give priority to invulnerables
- let num_missing_orchestrator_collators =
- min_num_orchestrator_chain.saturating_sub(new_assigned.orchestrator_chain.len());
- let invulnerables_for_orchestrator = T::RemoveInvulnerables::remove_invulnerables(
- &mut new_collators,
- num_missing_orchestrator_collators,
- );
- new_assigned.fill_orchestrator_chain_collators(
- min_num_orchestrator_chain,
- &mut invulnerables_for_orchestrator.into_iter(),
- );
- // If there are no enough invulnerables, or if the invulnerables are currently assigned to other chains,
- // fill orchestrator chain with regular collators
- let mut new_collators = new_collators.into_iter();
- new_assigned
- .fill_orchestrator_chain_collators(min_num_orchestrator_chain, &mut new_collators);
-
- // Fill container chain collators using new collators and also the extra
- // collators that were previously assigned to the orchestrator chain,
- // but give preference to new collators
- let mut extra_orchestrator_collators = extra_orchestrator_collators.into_iter();
- let mut new_plus_extra_collators = new_collators
- .by_ref()
- .chain(&mut extra_orchestrator_collators);
-
- new_assigned.add_and_fill_new_container_chains_in_order(
- num_each_container_chain,
- container_chain_ids,
- &mut new_plus_extra_collators,
- );
-
- // Fill orchestrator chain collators back up to max_num_orchestrator_chain,
- // but give preference to collators that were already there
- let mut extra_collators_plus_new = extra_orchestrator_collators
- .by_ref()
- .chain(&mut new_collators);
- new_assigned.fill_orchestrator_chain_collators(
- max_num_orchestrator_chain,
- &mut extra_collators_plus_new,
- );
-
- // Reorganize container chain collators to fill the maximum number of container
- // chains. For example, if num_each_container_chain == 2 and the number of collators
- // in each container chain is
- // [1, 1, 1, 1, 1]
- // Then we can convert that into
- // [2, 2, 0, 0, 0]
- // and assign 1 extra collator to the orchestrator chain, if needed.
- let incomplete_container_chains_collators = new_assigned
- .reorganize_incomplete_container_chains_collators(
- container_chain_ids,
- num_each_container_chain,
- );
-
- // Assign collators from container chains that do not reach
- // "num_each_container_chain" to orchestrator chain
- new_assigned.fill_orchestrator_chain_collators(
- max_num_orchestrator_chain,
- &mut incomplete_container_chains_collators.into_iter(),
- );
-
- new_assigned
- }
-
// Returns the assigned collators as read from storage.
// If there is any item in PendingCollatorContainerChain, returns that element.
// Otherwise, reads and returns the current CollatorContainerChain
diff --git a/pallets/collator-assignment/src/mock.rs b/pallets/collator-assignment/src/mock.rs
index f5c9e69ef..e7f9b31f8 100644
--- a/pallets/collator-assignment/src/mock.rs
+++ b/pallets/collator-assignment/src/mock.rs
@@ -25,7 +25,7 @@ use {
},
frame_system as system,
parity_scale_codec::{Decode, Encode},
- sp_core::H256,
+ sp_core::{Get, H256},
sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
@@ -111,9 +111,13 @@ pub struct Mocks {
pub min_orchestrator_chain_collators: u32,
pub max_orchestrator_chain_collators: u32,
pub collators_per_container: u32,
+ pub collators_per_parathread: u32,
pub collators: Vec,
pub container_chains: Vec,
+ pub parathreads: Vec,
pub random_seed: [u8; 32],
+ // None means 5
+ pub full_rotation_period: Option,
}
impl mock_data::Config for Test {}
@@ -123,7 +127,7 @@ impl mock_data::Config for Test {}
pub struct HostConfigurationGetter;
parameter_types! {
- pub const ParachainId: ParaId = ParaId::new(200);
+ pub const ParachainId: ParaId = ParaId::new(1000);
}
impl pallet_collator_assignment::GetHostConfiguration for HostConfigurationGetter {
@@ -138,6 +142,10 @@ impl pallet_collator_assignment::GetHostConfiguration for HostConfiguration
fn collators_per_container(_session_index: u32) -> u32 {
MockData::mock().collators_per_container
}
+
+ fn collators_per_parathread(_session_index: u32) -> u32 {
+ MockData::mock().collators_per_parathread
+ }
}
pub struct CollatorsGetter;
@@ -160,6 +168,15 @@ impl tp_traits::GetSessionContainerChains for ContainerChainsGetter {
.collect()
}
+ fn session_parathreads(_session_index: u32) -> Vec {
+ MockData::mock()
+ .parathreads
+ .iter()
+ .cloned()
+ .map(ParaId::from)
+ .collect()
+ }
+
#[cfg(feature = "runtime-benchmarks")]
fn set_session_container_chains(_session_index: u32, para_ids: &[ParaId]) {
MockData::mutate(|mocks| {
@@ -184,13 +201,22 @@ parameter_types! {
pub const CollatorRotationSessionPeriod: u32 = 5;
}
+pub struct MockCollatorRotationSessionPeriod;
+
+impl Get for MockCollatorRotationSessionPeriod {
+ fn get() -> u32 {
+ MockData::mock().full_rotation_period.unwrap_or(5)
+ }
+}
+
impl pallet_collator_assignment::Config for Test {
type RuntimeEvent = RuntimeEvent;
type SessionIndex = u32;
type HostConfiguration = HostConfigurationGetter;
type ContainerChains = ContainerChainsGetter;
type SelfParaId = ParachainId;
- type ShouldRotateAllCollators = RotateCollatorsEveryNSessions;
+ type ShouldRotateAllCollators =
+ RotateCollatorsEveryNSessions;
type GetRandomnessForNextBlock = MockGetRandomnessForNextBlock;
type RemoveInvulnerables = RemoveAccountIdsAbove100;
type RemoveParaIdsWithNoCredits = RemoveParaIdsAbove5000;
diff --git a/pallets/collator-assignment/src/tests.rs b/pallets/collator-assignment/src/tests.rs
index 5706ad1e3..b0bf4e29e 100644
--- a/pallets/collator-assignment/src/tests.rs
+++ b/pallets/collator-assignment/src/tests.rs
@@ -14,11 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Tanssi. If not, see
+use dp_collator_assignment::AssignedCollators;
use {
crate::{mock::*, CollatorContainerChain, Event, PendingCollatorContainerChain},
std::collections::BTreeMap,
};
+mod assign_full;
+mod prioritize_invulnerables;
+mod select_chains;
+
fn assigned_collators() -> BTreeMap {
let assigned_collators = CollatorContainerChain::::get();
@@ -31,7 +36,7 @@ fn assigned_collators() -> BTreeMap {
}
for collator in assigned_collators.orchestrator_chain {
- h.insert(collator, 999);
+ h.insert(collator, 1000);
}
h
@@ -44,6 +49,7 @@ fn assign_initial_collators() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -60,11 +66,11 @@ fn assign_initial_collators() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -81,6 +87,7 @@ fn assign_collators_after_one_leaves_container() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -97,11 +104,11 @@ fn assign_collators_after_one_leaves_container() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -120,11 +127,11 @@ fn assign_collators_after_one_leaves_container() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
//(6, 1001),
(7, 1001),
(8, 1002),
@@ -143,6 +150,7 @@ fn assign_collators_after_one_leaves_orchestrator_chain() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -156,11 +164,11 @@ fn assign_collators_after_one_leaves_orchestrator_chain() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -177,17 +185,17 @@ fn assign_collators_after_one_leaves_orchestrator_chain() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- //(4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ //(4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
(9, 1002),
// 10 is assigned in place of 4
- (10, 999),
+ (10, 1000),
]),
);
});
@@ -200,6 +208,7 @@ fn assign_collators_if_config_orchestrator_chain_collators_increases() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -212,11 +221,11 @@ fn assign_collators_if_config_orchestrator_chain_collators_increases() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -235,18 +244,18 @@ fn assign_collators_if_config_orchestrator_chain_collators_increases() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
(9, 1002),
- (10, 999),
- (11, 999),
- (12, 999),
+ (10, 1000),
+ (11, 1000),
+ (12, 1000),
]),
);
});
@@ -259,6 +268,7 @@ fn assign_collators_if_config_orchestrator_chain_collators_decreases() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -271,11 +281,11 @@ fn assign_collators_if_config_orchestrator_chain_collators_decreases() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -303,6 +313,7 @@ fn assign_collators_if_config_collators_per_container_increases() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -316,11 +327,11 @@ fn assign_collators_if_config_collators_per_container_increases() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -338,11 +349,11 @@ fn assign_collators_if_config_collators_per_container_increases() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -363,6 +374,7 @@ fn assign_collators_if_container_chain_is_removed() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -375,11 +387,11 @@ fn assign_collators_if_container_chain_is_removed() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -397,11 +409,11 @@ fn assign_collators_if_container_chain_is_removed() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
]),
@@ -416,6 +428,7 @@ fn assign_collators_if_container_chain_is_added() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -428,11 +441,11 @@ fn assign_collators_if_container_chain_is_added() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -450,11 +463,11 @@ fn assign_collators_if_container_chain_is_added() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -473,6 +486,7 @@ fn assign_collators_after_decrease_num_collators() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 5;
m.max_orchestrator_chain_collators = 5;
@@ -482,27 +496,26 @@ fn assign_collators_after_decrease_num_collators() {
assert_eq!(assigned_collators(), BTreeMap::new(),);
run_to_block(11);
- assert_eq!(
- assigned_collators(),
- BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
- (6, 1001),
- (7, 1001),
- (8, 1002),
- (9, 1002),
- ]),
- );
+ let initial_assignment = BTreeMap::from_iter(vec![
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
+ (6, 1001),
+ (7, 1001),
+ (8, 1002),
+ (9, 1002),
+ ]);
+ assert_eq!(assigned_collators(), initial_assignment,);
MockData::mutate(|m| {
m.collators = vec![];
});
run_to_block(21);
- assert_eq!(assigned_collators(), BTreeMap::from_iter(vec![]));
+ // There are no collators but that would brick the chain, so we keep the old assignment
+ assert_eq!(assigned_collators(), initial_assignment);
});
}
@@ -513,6 +526,7 @@ fn assign_collators_stay_constant_if_new_collators_can_take_new_chains() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -524,7 +538,7 @@ fn assign_collators_stay_constant_if_new_collators_can_take_new_chains() {
assert_eq!(
assigned_collators(),
- BTreeMap::from_iter(vec![(1, 999), (2, 999), (3, 999), (4, 999), (5, 999),]),
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 1000), (4, 1000), (5, 1000),]),
);
MockData::mutate(|m| {
@@ -535,11 +549,11 @@ fn assign_collators_stay_constant_if_new_collators_can_take_new_chains() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 999),
- (4, 999),
- (5, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
(6, 1001),
(7, 1001),
(8, 1002),
@@ -556,6 +570,7 @@ fn assign_collators_move_extra_container_chain_to_orchestrator_chain_if_not_enou
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -567,7 +582,7 @@ fn assign_collators_move_extra_container_chain_to_orchestrator_chain_if_not_enou
assert_eq!(
assigned_collators(),
- BTreeMap::from_iter(vec![(1, 999), (2, 999), (3, 999), (4, 999),]),
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 1000), (4, 1000),]),
);
MockData::mutate(|m| {
@@ -578,7 +593,7 @@ fn assign_collators_move_extra_container_chain_to_orchestrator_chain_if_not_enou
assert_eq!(
assigned_collators(),
- BTreeMap::from_iter(vec![(1, 999), (2, 999), (5, 1001), (3, 1001), (4, 999),]),
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 1000), (4, 1001), (5, 1001),]),
);
});
}
@@ -590,6 +605,7 @@ fn assign_collators_reorganize_container_chains_if_not_enough_collators() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -602,8 +618,8 @@ fn assign_collators_reorganize_container_chains_if_not_enough_collators() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
+ (1, 1000),
+ (2, 1000),
(3, 1001),
(4, 1001),
(5, 1002),
@@ -627,13 +643,13 @@ fn assign_collators_reorganize_container_chains_if_not_enough_collators() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 1005),
- (5, 1004),
- (7, 999),
- (9, 1004),
- (11, 1005)
+ (1, 1000),
+ (2, 1000),
+ (3, 1001),
+ (5, 1002),
+ (7, 1000),
+ (9, 1001),
+ (11, 1002)
]),
);
});
@@ -646,6 +662,7 @@ fn assign_collators_set_zero_per_container() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -658,18 +675,18 @@ fn assign_collators_set_zero_per_container() {
assert_eq!(
assigned_collators(),
BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 1001),
- (4, 1001),
- (5, 1002),
- (6, 1002),
- (7, 1003),
- (8, 1003),
- (9, 1004),
- (10, 1004),
- (11, 999),
- (12, 999)
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1001),
+ (6, 1001),
+ (7, 1002),
+ (8, 1002),
+ (9, 1003),
+ (10, 1003),
+ (11, 1004),
+ (12, 1004),
]),
);
@@ -682,7 +699,7 @@ fn assign_collators_set_zero_per_container() {
// There are 5 collators in total: 0x4 container chains, plus 5 in the orchestrator chain
assert_eq!(
assigned_collators(),
- BTreeMap::from_iter(vec![(1, 999), (2, 999), (3, 999), (11, 999), (12, 999),]),
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 1000), (4, 1000), (5, 1000),]),
);
});
}
@@ -694,6 +711,7 @@ fn assign_collators_rotation() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -704,18 +722,18 @@ fn assign_collators_rotation() {
run_to_block(11);
let initial_assignment = BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 1001),
- (4, 1001),
- (5, 1002),
- (6, 1002),
- (7, 1003),
- (8, 1003),
- (9, 1004),
- (10, 1004),
- (11, 999),
- (12, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1001),
+ (6, 1001),
+ (7, 1002),
+ (8, 1002),
+ (9, 1003),
+ (10, 1003),
+ (11, 1004),
+ (12, 1004),
]);
assert_eq!(assigned_collators(), initial_assignment,);
@@ -743,18 +761,18 @@ fn assign_collators_rotation() {
// Random assignment depends on the seed, shouldn't change unless the algorithm changes
let shuffled_assignment = BTreeMap::from_iter(vec![
- (1, 1004),
- (2, 999),
- (3, 999),
- (4, 1003),
- (5, 1001),
- (6, 1001),
- (7, 999),
- (8, 1002),
- (9, 999),
- (10, 1003),
- (11, 1004),
- (12, 1002),
+ (1, 1000),
+ (2, 1002),
+ (3, 1000),
+ (4, 1004),
+ (5, 1003),
+ (6, 1003),
+ (7, 1000),
+ (8, 1001),
+ (9, 1002),
+ (10, 1004),
+ (11, 1000),
+ (12, 1001),
]);
assert_eq!(assigned_collators(), shuffled_assignment,);
@@ -768,6 +786,7 @@ fn assign_collators_rotation_container_chains_are_shuffled() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -779,7 +798,7 @@ fn assign_collators_rotation_container_chains_are_shuffled() {
run_to_block(11);
let initial_assignment =
- BTreeMap::from_iter(vec![(1, 999), (2, 999), (3, 1001), (4, 1001)]);
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 1001), (4, 1001)]);
assert_eq!(assigned_collators(), initial_assignment,);
@@ -793,7 +812,46 @@ fn assign_collators_rotation_container_chains_are_shuffled() {
// Random assignment depends on the seed, shouldn't change unless the algorithm changes
// Test that container chains are shuffled because 1001 does not have priority
let shuffled_assignment =
- BTreeMap::from_iter(vec![(1, 999), (2, 1002), (3, 999), (4, 1002)]);
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1002), (3, 1000), (4, 1002)]);
+
+ assert_eq!(assigned_collators(), shuffled_assignment,);
+ });
+}
+
+#[test]
+fn assign_collators_rotation_parathreads_are_shuffled() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+
+ MockData::mutate(|m| {
+ m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
+ m.min_orchestrator_chain_collators = 2;
+ m.max_orchestrator_chain_collators = 5;
+
+ // 4 collators so we can only assign to one parathread
+ m.collators = vec![1, 2, 3, 4];
+ m.parathreads = vec![5001, 5002];
+ });
+ assert_eq!(assigned_collators(), BTreeMap::new(),);
+ run_to_block(11);
+
+ let initial_assignment =
+ BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 5001), (4, 5001)]);
+
+ assert_eq!(assigned_collators(), initial_assignment,);
+
+ MockData::mutate(|m| {
+ // Seed chosen manually to see the case where parathread 5002 is given priority
+ m.random_seed = [2; 32];
+ });
+
+ run_to_block(26);
+
+ // Random assignment depends on the seed, shouldn't change unless the algorithm changes
+ // Test that container chains are shuffled because 1001 does not have priority
+ let shuffled_assignment =
+ BTreeMap::from_iter(vec![(1, 1000), (2, 5002), (3, 1000), (4, 5002)]);
assert_eq!(assigned_collators(), shuffled_assignment,);
});
@@ -806,6 +864,7 @@ fn assign_collators_rotation_collators_are_shuffled() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -817,15 +876,15 @@ fn assign_collators_rotation_collators_are_shuffled() {
run_to_block(11);
let initial_assignment = BTreeMap::from_iter(vec![
- (1, 999),
- (2, 999),
- (3, 1001),
- (4, 1001),
- (5, 1002),
- (6, 1002),
- (7, 999),
- (8, 999),
- (9, 999),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1000),
+ (6, 1001),
+ (7, 1001),
+ (8, 1002),
+ (9, 1002),
]);
assert_eq!(assigned_collators(), initial_assignment,);
@@ -840,15 +899,15 @@ fn assign_collators_rotation_collators_are_shuffled() {
// Test that collators are shuffled because collator 10 should be the last one to be assigned,
// and here it is present
let shuffled_assignment = BTreeMap::from_iter(vec![
- (1, 999),
- (3, 1001),
- (4, 1001),
- (5, 1002),
- (6, 999),
- (7, 999),
- (8, 999),
- (9, 1002),
- (10, 999),
+ (1, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1001),
+ (6, 1002),
+ (7, 1000),
+ (8, 1001),
+ (9, 1000),
+ (10, 1002),
]);
assert_eq!(assigned_collators(), shuffled_assignment,);
@@ -862,6 +921,7 @@ fn assign_collators_invulnerables_priority_orchestrator() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -874,18 +934,97 @@ fn assign_collators_invulnerables_priority_orchestrator() {
run_to_block(11);
let initial_assignment = BTreeMap::from_iter(vec![
- (100, 999),
- (1, 999),
- (2, 1001),
- (3, 1001),
- (4, 1002),
- (5, 1002),
- (6, 999),
- (7, 999),
- (8, 999),
+ (100, 1000),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1000),
+ (5, 1001),
+ (6, 1001),
+ (7, 1002),
+ (8, 1002),
+ ]);
+
+ assert_eq!(assigned_collators(), initial_assignment,);
+ });
+}
+
+#[test]
+fn assign_collators_invulnerables_priority_orchestrator_reassigned() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+
+ MockData::mutate(|m| {
+ m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
+ m.min_orchestrator_chain_collators = 2;
+ m.max_orchestrator_chain_collators = 5;
+ // Disable rotation because this test is long
+ m.full_rotation_period = Some(0);
+
+ // 10 collators but we only need 9, so 1 collator will not be assigned
+ // ids >= 100 are invulnerables so 2 of them will always be assigned to the orchestrator
+ m.collators = vec![1, 2, 3, 4, 5, 100, 101, 102, 103, 104];
+ m.container_chains = vec![1001, 1002];
+ });
+ assert_eq!(assigned_collators(), BTreeMap::new(),);
+ run_to_block(11);
+
+ let initial_assignment = BTreeMap::from_iter(vec![
+ (100, 1000),
+ (101, 1000),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1001),
+ (5, 1001),
+ (102, 1002),
+ (103, 1002),
]);
assert_eq!(assigned_collators(), initial_assignment,);
+
+ MockData::mutate(|m| {
+ // Remove invulnerable from orchestrator, the unassigned invulnerable will take its place
+ m.collators = vec![1, 2, 3, 4, 5, 101, 102, 103, 104];
+ });
+
+ run_to_block(21);
+
+ let assignment = BTreeMap::from_iter(vec![
+ (104, 1000),
+ (101, 1000),
+ (1, 1000),
+ (2, 1000),
+ (3, 1000),
+ (4, 1001),
+ (5, 1001),
+ (102, 1002),
+ (103, 1002),
+ ]);
+
+ assert_eq!(assigned_collators(), assignment,);
+
+ MockData::mutate(|m| {
+ // Remove another invulnerable from orchestrator, there are no unassigned invulnerables so the ones in a
+ // container chain will move from the container chain to the orchestrator
+ m.collators = vec![1, 2, 3, 4, 5, 102, 103, 104];
+ });
+
+ run_to_block(31);
+
+ let assignment = BTreeMap::from_iter(vec![
+ (104, 1000),
+ (102, 1000),
+ (1, 1000),
+ (2, 1000),
+ (3, 1002),
+ (4, 1001),
+ (5, 1001),
+ (103, 1002),
+ ]);
+
+ assert_eq!(assigned_collators(), assignment,);
});
}
@@ -896,6 +1035,7 @@ fn assign_collators_all_invulnerables() {
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -907,15 +1047,15 @@ fn assign_collators_all_invulnerables() {
run_to_block(11);
let initial_assignment = BTreeMap::from_iter(vec![
- (101, 999),
- (102, 999),
- (103, 1001),
- (104, 1001),
- (105, 1002),
- (106, 1002),
- (107, 999),
- (108, 999),
- (109, 999),
+ (101, 1000),
+ (102, 1000),
+ (103, 1000),
+ (104, 1000),
+ (105, 1000),
+ (106, 1001),
+ (107, 1001),
+ (108, 1002),
+ (109, 1002),
]);
assert_eq!(assigned_collators(), initial_assignment,);
@@ -924,12 +1064,13 @@ fn assign_collators_all_invulnerables() {
#[test]
fn rotation_events() {
- // Ensure that the NewPendingAssignment is correct
+ // Ensure that the NewPendingAssignment event is correct
new_test_ext().execute_with(|| {
run_to_block(1);
MockData::mutate(|m| {
m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
m.min_orchestrator_chain_collators = 2;
m.max_orchestrator_chain_collators = 5;
@@ -1014,3 +1155,89 @@ fn rotation_events() {
}
});
}
+
+#[test]
+fn assign_collators_remove_from_orchestator_when_all_assigned() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+
+ MockData::mutate(|m| {
+ m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
+ m.min_orchestrator_chain_collators = 2;
+ m.max_orchestrator_chain_collators = 2;
+
+ m.collators = vec![1, 2];
+ m.container_chains = vec![1001];
+ });
+ assert_eq!(assigned_collators(), BTreeMap::new(),);
+ run_to_block(11);
+
+ let initial_assignment = BTreeMap::from_iter(vec![(1, 1000), (2, 1000)]);
+
+ assert_eq!(assigned_collators(), initial_assignment,);
+
+ MockData::mutate(|m| {
+ m.collators = vec![1, 2, 3, 4];
+ });
+
+ run_to_block(26);
+
+ let assignment = BTreeMap::from_iter(vec![(1, 1000), (2, 1000), (3, 1001), (4, 1001)]);
+ assert_eq!(assigned_collators(), assignment,);
+
+ MockData::mutate(|m| {
+ m.collators = vec![1, 3, 4];
+ });
+
+ run_to_block(36);
+
+ let assignment = BTreeMap::from_iter(vec![(1, 1000), (3, 1000)]);
+
+ assert_eq!(assigned_collators(), assignment,);
+
+ MockData::mutate(|m| {
+ m.collators = vec![3, 4];
+ });
+
+ run_to_block(46);
+
+ let assignment = BTreeMap::from_iter(vec![(3, 1000), (4, 1000)]);
+
+ assert_eq!(assigned_collators(), assignment,);
+ });
+}
+
+#[test]
+fn collator_assignment_includes_empty_chains() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+
+ MockData::mutate(|m| {
+ m.collators_per_container = 2;
+ m.collators_per_parathread = 2;
+ m.min_orchestrator_chain_collators = 2;
+ m.max_orchestrator_chain_collators = 2;
+
+ m.collators = vec![1, 2];
+ m.container_chains = vec![2000, 2001, 2002];
+ m.parathreads = vec![3000, 3001, 3002]
+ });
+ assert_eq!(assigned_collators(), BTreeMap::new(),);
+ run_to_block(11);
+
+ let assigned_collators = CollatorContainerChain::::get();
+ let expected = AssignedCollators {
+ orchestrator_chain: vec![1, 2],
+ container_chains: BTreeMap::from_iter(vec![
+ (2000.into(), vec![]),
+ (2001.into(), vec![]),
+ (2002.into(), vec![]),
+ (3000.into(), vec![]),
+ (3001.into(), vec![]),
+ (3002.into(), vec![]),
+ ]),
+ };
+ assert_eq!(assigned_collators, expected);
+ });
+}
diff --git a/pallets/collator-assignment/src/tests/assign_full.rs b/pallets/collator-assignment/src/tests/assign_full.rs
new file mode 100644
index 000000000..2ac36e2f3
--- /dev/null
+++ b/pallets/collator-assignment/src/tests/assign_full.rs
@@ -0,0 +1,90 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+use crate::assignment::AssignmentError;
+use {
+ crate::{assignment::Assignment, tests::Test},
+ sp_std::collections::btree_map::BTreeMap,
+};
+
+#[test]
+fn assign_full_old_assigned_priority() {
+ // Collators in old_assigned will be selected before other collators
+ let collators = vec![1, 2, 3, 4, 5];
+ let container_chains = vec![(1000.into(), 5)];
+ let old_assigned = BTreeMap::from_iter(vec![(1000.into(), vec![3, 4])]);
+
+ let new_assigned =
+ Assignment::::assign_full(collators, container_chains, old_assigned).unwrap();
+ let expected = BTreeMap::from_iter(vec![(1000.into(), vec![3, 4, 1, 2, 5])]);
+ assert_eq!(new_assigned, expected);
+}
+
+#[test]
+fn assign_full_invalid_old_assigned_collators_removed() {
+ // If the collators in old_assigned are no longer collators, they are not assigned
+ let collators = vec![1, 2, 3, 4, 5];
+ let container_chains = vec![(1000.into(), 5)];
+ let old_assigned = BTreeMap::from_iter(vec![(1000.into(), vec![20, 21])]);
+
+ let new_assigned =
+ Assignment::::assign_full(collators, container_chains, old_assigned).unwrap();
+ let expected = BTreeMap::from_iter(vec![(1000.into(), vec![1, 2, 3, 4, 5])]);
+ assert_eq!(new_assigned, expected);
+}
+
+#[test]
+fn assign_full_invalid_chains_removed() {
+ // Mark all collators as already assigned to a chain that does not exist. Should treat them as not assigned.
+ let collators = vec![1, 2, 3, 4, 5];
+ let container_chains = vec![(1000.into(), 5)];
+ let old_assigned = BTreeMap::from_iter(vec![(1001.into(), vec![1, 2, 3, 4, 5])]);
+
+ let new_assigned =
+ Assignment::::assign_full(collators, container_chains, old_assigned).unwrap();
+ let expected = BTreeMap::from_iter(vec![(1000.into(), vec![1, 2, 3, 4, 5])]);
+ assert_eq!(new_assigned, expected);
+}
+
+#[test]
+fn assign_full_truncates_collators() {
+ // Need 2 collators for each chain, when old_assigned has more than 2. Should truncate old_assigned to 2.
+ let collators = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+ let container_chains = vec![(1000.into(), 2), (2000.into(), 2)];
+ let old_assigned = BTreeMap::from_iter(vec![
+ (1000.into(), vec![1, 2, 3, 4, 5]),
+ (2000.into(), vec![6, 7, 8, 9, 10]),
+ ]);
+
+ let new_assigned =
+ Assignment::::assign_full(collators, container_chains, old_assigned).unwrap();
+ let expected = BTreeMap::from_iter(vec![(1000.into(), vec![1, 2]), (2000.into(), vec![6, 7])]);
+ assert_eq!(new_assigned, expected);
+}
+
+#[test]
+fn assign_full_old_assigned_error_if_not_enough_collators() {
+ // Need 4 collators, only have 2, and all 2 were assigned to the second chain. If the function did not panic, we
+ // would have 0 collators assigned to the first chain, which is supposed to have priority.
+ let collators = vec![1, 2];
+ let container_chains = vec![(1000.into(), 2), (2000.into(), 2)];
+ let old_assigned = BTreeMap::from_iter(vec![(2000.into(), vec![1, 2])]);
+ let new_assigned = Assignment::::assign_full(collators, container_chains, old_assigned);
+ assert_eq!(
+ new_assigned.unwrap_err(),
+ AssignmentError::NotEnoughCollators
+ );
+}
diff --git a/pallets/collator-assignment/src/tests/prioritize_invulnerables.rs b/pallets/collator-assignment/src/tests/prioritize_invulnerables.rs
new file mode 100644
index 000000000..b9c57769b
--- /dev/null
+++ b/pallets/collator-assignment/src/tests/prioritize_invulnerables.rs
@@ -0,0 +1,186 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+use {
+ crate::{
+ assignment::{Assignment, ChainNumCollators},
+ tests::Test,
+ },
+ sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet},
+};
+
+#[test]
+fn invulnerable_priority_0_collators() {
+ let collators = vec![];
+ let orchestrator_chain = ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ };
+ let mut old_assigned = BTreeMap::new();
+
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 0);
+}
+
+#[test]
+fn invulnerable_priority_0_invulnerables() {
+ let collators = vec![1, 2, 3, 4, 5];
+ let orchestrator_chain = ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ };
+ let mut old_assigned = BTreeMap::from_iter(vec![(1000.into(), vec![1, 2])]);
+
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 0);
+}
+
+#[test]
+fn invulnerable_priority_1_invulnerable_orchestrator() {
+ let collators = vec![1, 2, 3, 4, 5, 101];
+ let orchestrator_chain = ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ };
+ let mut old_assigned = BTreeMap::from_iter(vec![(1000.into(), vec![101])]);
+
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 1);
+}
+
+#[test]
+fn invulnerable_priority_1_invulnerable_not_assigned() {
+ let collators = vec![1, 2, 3, 4, 5, 101];
+ let orchestrator_chain = ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ };
+ let mut old_assigned = BTreeMap::new();
+
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 1);
+}
+
+#[test]
+fn invulnerable_priority_1_invulnerable_assigned_to_another_chain() {
+ let collators = vec![1, 2, 3, 4, 5, 101];
+ let orchestrator_chain = ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ };
+ let mut old_assigned =
+ BTreeMap::from_iter(vec![(1000.into(), vec![]), (2000.into(), vec![101])]);
+
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 1);
+}
+
+#[test]
+fn bug_same_invulnerable_selected_twice() {
+ let collators = vec![100];
+ let orchestrator_chain = ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ };
+ let mut old_assigned = BTreeMap::from_iter(vec![(1000.into(), vec![100])]);
+
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 1);
+}
+
+#[test]
+fn bug_not_using_assigned_invulnerables() {
+ // There are 3 invulnerables, 1 assigned to orchestrator and 2 assigned to a container chain.
+ // After `prioritize_invulnerables` the first one from the container should move to orchestrator
+ let collators = vec![1, 2, 3, 4, 5, 102, 103, 104];
+
+ let container_chains = vec![
+ ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2000.into(),
+ min_collators: 2,
+ max_collators: 2,
+ },
+ ChainNumCollators {
+ para_id: 2001.into(),
+ min_collators: 2,
+ max_collators: 2,
+ },
+ ];
+ let orchestrator_chain = container_chains[0];
+
+ let mut old_assigned = BTreeMap::from_iter(vec![
+ (1000.into(), vec![101, 104, 1, 2, 3]),
+ (2000.into(), vec![4, 5]),
+ (2001.into(), vec![102, 103]),
+ ]);
+
+ let chains_with_collators_set =
+ BTreeSet::from_iter(container_chains.iter().map(|cc| cc.para_id));
+ let collators_set = BTreeSet::from_iter(collators.iter().cloned());
+ Assignment::::retain_valid_old_assigned(
+ &mut old_assigned,
+ &chains_with_collators_set,
+ &collators_set,
+ );
+ let num_invulnerables = Assignment::::prioritize_invulnerables(
+ &collators,
+ orchestrator_chain,
+ &mut old_assigned,
+ );
+
+ assert_eq!(num_invulnerables, 2);
+}
diff --git a/pallets/collator-assignment/src/tests/select_chains.rs b/pallets/collator-assignment/src/tests/select_chains.rs
new file mode 100644
index 000000000..c524c0c86
--- /dev/null
+++ b/pallets/collator-assignment/src/tests/select_chains.rs
@@ -0,0 +1,176 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Tanssi is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Tanssi. If not, see
+
+use crate::{
+ assignment::{Assignment, ChainNumCollators},
+ tests::Test,
+};
+
+#[test]
+fn select_chains_not_enough_to_reach_min_container() {
+ // 10 collators when the orchestrator needs 2 and the containers need 10 result in no containers having collators
+ let container_chains = vec![
+ ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2000.into(),
+ min_collators: 10,
+ max_collators: 10,
+ },
+ ChainNumCollators {
+ para_id: 2001.into(),
+ min_collators: 10,
+ max_collators: 10,
+ },
+ ];
+ let new_assigned = Assignment::::select_chains_with_collators(10, &container_chains);
+ assert_eq!(new_assigned, vec![(1000.into(), 5),]);
+}
+
+#[test]
+fn select_chains_not_enough_to_reach_min_orchestrator() {
+ // 1 collator when the orchestrator needs 2 results in 1 collators being assigned to orchestrator
+ let container_chains = vec![ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ }];
+ let new_assigned = Assignment::::select_chains_with_collators(1, &container_chains);
+ assert_eq!(new_assigned, vec![(1000.into(), 1),]);
+}
+
+#[test]
+fn select_chains_not_enough_for_all_min() {
+ // Need 6 collators to support 3 chains, only have 5. The last chain will be removed and the remaining collator
+ // will be assigned to orchestrator.
+ let container_chains = vec![
+ ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2000.into(),
+ min_collators: 2,
+ max_collators: 2,
+ },
+ ChainNumCollators {
+ para_id: 2001.into(),
+ min_collators: 2,
+ max_collators: 2,
+ },
+ ];
+ let new_assigned = Assignment::::select_chains_with_collators(5, &container_chains);
+ assert_eq!(new_assigned, vec![(1000.into(), 3), (2000.into(), 2),]);
+}
+
+#[test]
+fn select_chains_not_enough_for_all_max() {
+ // Need 6 collators to support 3 chains at min, but 15 collators to support them at max.
+ // The last chain will be removed and the remaining collator
+ let container_chains = vec![
+ ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2001.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ];
+ let new_assigned = Assignment::::select_chains_with_collators(7, &container_chains);
+ assert_eq!(
+ new_assigned,
+ vec![(1000.into(), 3), (2000.into(), 2), (2001.into(), 2),]
+ );
+ let new_assigned = Assignment::::select_chains_with_collators(10, &container_chains);
+ assert_eq!(
+ new_assigned,
+ vec![(1000.into(), 5), (2000.into(), 3), (2001.into(), 2),]
+ );
+ let new_assigned = Assignment::::select_chains_with_collators(13, &container_chains);
+ assert_eq!(
+ new_assigned,
+ vec![(1000.into(), 5), (2000.into(), 5), (2001.into(), 3),]
+ );
+ let new_assigned = Assignment::::select_chains_with_collators(15, &container_chains);
+ assert_eq!(
+ new_assigned,
+ vec![(1000.into(), 5), (2000.into(), 5), (2001.into(), 5),]
+ );
+}
+
+#[test]
+fn select_chains_more_than_max() {
+ // When the number of collators is greater than the sum of the max, all the chains are assigned max collators
+ let container_chains = vec![
+ ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2001.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ];
+ let new_assigned = Assignment::::select_chains_with_collators(20, &container_chains);
+ assert_eq!(
+ new_assigned,
+ vec![(1000.into(), 5), (2000.into(), 5), (2001.into(), 5),]
+ );
+}
+
+#[test]
+fn select_chains_not_enough_to_reach_min_container_but_enough_for_parathread() {
+ // Chain 2000 has more priority than parathread 3000, but we do not have enough min collators so the container
+ // chain gets 0 collator and the parathread gets 1
+ let container_chains = vec![
+ ChainNumCollators {
+ para_id: 1000.into(),
+ min_collators: 2,
+ max_collators: 5,
+ },
+ ChainNumCollators {
+ para_id: 2000.into(),
+ min_collators: 2,
+ max_collators: 2,
+ },
+ ChainNumCollators {
+ para_id: 3000.into(),
+ min_collators: 1,
+ max_collators: 1,
+ },
+ ];
+ let new_assigned = Assignment::::select_chains_with_collators(3, &container_chains);
+ assert_eq!(new_assigned, vec![(1000.into(), 2), (3000.into(), 1)]);
+}
diff --git a/pallets/configuration/src/lib.rs b/pallets/configuration/src/lib.rs
index ecbcd6d87..0a7a035ca 100644
--- a/pallets/configuration/src/lib.rs
+++ b/pallets/configuration/src/lib.rs
@@ -74,9 +74,12 @@ pub struct HostConfiguration {
pub max_orchestrator_collators: u32,
/// How many collators to assign to one container chain
pub collators_per_container: u32,
- /// If this value is 0 means that there is no rotation
+ /// Rotate all collators once every n sessions. If this value is 0 means that there is no rotation
pub full_rotation_period: u32,
/// How many collators to assign to one parathread
+ // TODO: for now we only support 1 collator per parathread because using Aura for consensus conflicts with
+ // the idea of being able to create blocks every n slots: if there are 2 collators and we create blocks
+ // every 2 slots, 1 collator will create all the blocks.
pub collators_per_parathread: u32,
/// How many parathreads can be assigned to one collator
pub parathreads_per_collator: u32,
@@ -535,6 +538,19 @@ pub mod pallet {
config.collators_per_container
}
+ fn collators_per_parathread(session_index: T::SessionIndex) -> u32 {
+ let (past_and_present, _) = Pallet::::pending_configs()
+ .into_iter()
+ .partition::, _>(|&(apply_at_session, _)| apply_at_session <= session_index);
+
+ let config = if let Some(last) = past_and_present.last() {
+ last.1.clone()
+ } else {
+ Pallet::::config()
+ };
+ config.collators_per_parathread
+ }
+
fn min_collators_for_orchestrator(session_index: T::SessionIndex) -> u32 {
let (past_and_present, _) = Pallet::::pending_configs()
.into_iter()
diff --git a/pallets/registrar/src/lib.rs b/pallets/registrar/src/lib.rs
index 3dc3d8b38..994cd4644 100644
--- a/pallets/registrar/src/lib.rs
+++ b/pallets/registrar/src/lib.rs
@@ -905,6 +905,11 @@ pub mod pallet {
paras.into_iter().collect()
}
+ fn session_parathreads(_session_index: T::SessionIndex) -> Vec {
+ // FIXME(parathreads)
+ vec![]
+ }
+
#[cfg(feature = "runtime-benchmarks")]
fn set_session_container_chains(
_session_index: T::SessionIndex,
diff --git a/primitives/traits/src/lib.rs b/primitives/traits/src/lib.rs
index 32aba7f6c..3b0537cee 100644
--- a/primitives/traits/src/lib.rs
+++ b/primitives/traits/src/lib.rs
@@ -76,6 +76,7 @@ pub trait GetCurrentContainerChains {
/// session index.
pub trait GetSessionContainerChains {
fn session_container_chains(session_index: SessionIndex) -> Vec;
+ fn session_parathreads(session_index: SessionIndex) -> Vec;
#[cfg(feature = "runtime-benchmarks")]
fn set_session_container_chains(session_index: SessionIndex, container_chains: &[ParaId]);
}
@@ -93,6 +94,7 @@ pub trait GetHostConfiguration {
fn min_collators_for_orchestrator(session_index: SessionIndex) -> u32;
fn max_collators_for_orchestrator(session_index: SessionIndex) -> u32;
fn collators_per_container(session_index: SessionIndex) -> u32;
+ fn collators_per_parathread(session_index: SessionIndex) -> u32;
}
/// Returns current session index.
diff --git a/runtime/dancebox/tests/integration_test.rs b/runtime/dancebox/tests/integration_test.rs
index c5843a778..4f340e3e2 100644
--- a/runtime/dancebox/tests/integration_test.rs
+++ b/runtime/dancebox/tests/integration_test.rs
@@ -1297,12 +1297,9 @@ fn test_author_collation_aura_add_assigned_to_paras_runtime_api() {
run_to_session(4u32);
assert_eq!(
Runtime::parachain_collators(100.into()),
- Some(vec![ALICE.into()])
- );
- assert_eq!(
- Runtime::parachain_collators(1001.into()),
- Some(vec![CHARLIE.into(), DAVE.into()])
+ Some(vec![ALICE.into(), CHARLIE.into()])
);
+ assert_eq!(Runtime::parachain_collators(1001.into()), Some(vec![]));
assert_eq!(
Runtime::current_collator_parachain_assignment(BOB.into()),
None
diff --git a/runtime/flashbox/tests/integration_test.rs b/runtime/flashbox/tests/integration_test.rs
index 8ff1c6e36..59dd94bb8 100644
--- a/runtime/flashbox/tests/integration_test.rs
+++ b/runtime/flashbox/tests/integration_test.rs
@@ -1283,12 +1283,9 @@ fn test_author_collation_aura_add_assigned_to_paras_runtime_api() {
run_to_session(4u32);
assert_eq!(
Runtime::parachain_collators(100.into()),
- Some(vec![ALICE.into()])
- );
- assert_eq!(
- Runtime::parachain_collators(1001.into()),
- Some(vec![CHARLIE.into(), DAVE.into()])
+ Some(vec![ALICE.into(), CHARLIE.into()])
);
+ assert_eq!(Runtime::parachain_collators(1001.into()), Some(vec![]));
assert_eq!(
Runtime::current_collator_parachain_assignment(BOB.into()),
None
diff --git a/test/scripts/build-spec-warp-sync.sh b/test/scripts/build-spec-warp-sync.sh
index ec4e70c49..0215a1942 100755
--- a/test/scripts/build-spec-warp-sync.sh
+++ b/test/scripts/build-spec-warp-sync.sh
@@ -8,4 +8,4 @@ cd $(dirname $0)/..
mkdir -p specs
../target/release/container-chain-template-simple-node build-spec --disable-default-bootnode --add-bootnode "/ip4/127.0.0.1/tcp/33049/ws/p2p/12D3KooWHVMhQDHBpj9vQmssgyfspYecgV6e3hH1dQVDUkUbCYC9" --parachain-id 2000 --raw > specs/warp-sync-template-container-2000.json
-../target/release/tanssi-node build-spec --chain dancebox-local --parachain-id 1000 --add-container-chain specs/warp-sync-template-container-2000.json --invulnerable "Collator1000-01" --invulnerable "Collator1000-02" --invulnerable "Collator2000-01" --invulnerable "Collator2000-02" --invulnerable "Collator1000-03" > specs/warp-sync-tanssi-1000.json
+../target/release/tanssi-node build-spec --chain dancebox-local --parachain-id 1000 --add-container-chain specs/warp-sync-template-container-2000.json --invulnerable "Collator1000-01" --invulnerable "Collator1000-02" --invulnerable "Collator1000-03" --invulnerable "Collator2000-01" --invulnerable "Collator2000-02" > specs/warp-sync-tanssi-1000.json
diff --git a/test/scripts/build-spec.sh b/test/scripts/build-spec.sh
index d95622194..b02ab2375 100755
--- a/test/scripts/build-spec.sh
+++ b/test/scripts/build-spec.sh
@@ -10,4 +10,4 @@ mkdir -p specs
../target/release/container-chain-template-simple-node build-spec --disable-default-bootnode --add-bootnode "/ip4/127.0.0.1/tcp/33049/ws/p2p/12D3KooWHVMhQDHBpj9vQmssgyfspYecgV6e3hH1dQVDUkUbCYC9" --parachain-id 2000 --raw > specs/template-container-2000.json
../target/release/container-chain-template-frontier-node build-spec --disable-default-bootnode --add-bootnode "/ip4/127.0.0.1/tcp/33050/ws/p2p/12D3KooWFGaw1rxB6MSuN3ucuBm7hMq5pBFJbEoqTyth4cG483Cc" --parachain-id 2001 --raw > specs/template-container-2001.json
../target/release/container-chain-template-simple-node build-spec --disable-default-bootnode --parachain-id 2002 --raw > specs/template-container-2002.json
-../target/release/tanssi-node build-spec --chain dancebox-local --parachain-id 1000 --add-container-chain specs/template-container-2000.json --add-container-chain specs/template-container-2001.json --invulnerable "Collator1000-01" --invulnerable "Collator1000-02" --invulnerable "Collator2000-01" --invulnerable "Collator2000-02" --invulnerable "Collator2001-01" --invulnerable "Collator2001-02" --invulnerable "Collator2002-01" --invulnerable "Collator2002-02" > specs/tanssi-1000.json
+../target/release/tanssi-node build-spec --chain dancebox-local --parachain-id 1000 --add-container-chain specs/template-container-2000.json --add-container-chain specs/template-container-2001.json --invulnerable "Collator1000-01" --invulnerable "Collator1000-02" --invulnerable "Collator2002-01" --invulnerable "Collator2002-02" --invulnerable "Collator2000-01" --invulnerable "Collator2000-02" --invulnerable "Collator2001-01" --invulnerable "Collator2001-02" > specs/tanssi-1000.json
diff --git a/test/suites/dev-tanssi/session-keys/test_remove_session_key_staking.ts b/test/suites/dev-tanssi/session-keys/test_remove_session_key_staking.ts
index be36e8601..3b64c8970 100644
--- a/test/suites/dev-tanssi/session-keys/test_remove_session_key_staking.ts
+++ b/test/suites/dev-tanssi/session-keys/test_remove_session_key_staking.ts
@@ -3,7 +3,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli";
import { KeyringPair } from "@moonwall/util";
import { ApiPromise } from "@polkadot/api";
import { jumpSessions } from "../../../util/block";
-import { DANCE, STAKING_ACCOUNT } from "util/constants";
+import { DANCE } from "util/constants";
describeSuite({
id: "DT0202",
@@ -13,10 +13,12 @@ describeSuite({
let polkadotJs: ApiPromise;
let alice: KeyringPair;
let bob: KeyringPair;
+ let charlie: KeyringPair;
beforeAll(async () => {
alice = context.keyring.alice;
bob = context.keyring.bob;
+ charlie = context.keyring.charlie;
polkadotJs = context.polkadotJs();
let aliceNonce = (await polkadotJs.rpc.system.accountNextIndex(alice.address)).toNumber();
@@ -25,16 +27,13 @@ describeSuite({
// We need to remove from invulnerables and add to staking
// for that we need to remove Alice and Bob from invulnerables first
- // Additionally, we need to pass to the staking account the minimum balance
// We delegate with manual rewards to make sure the candidate does not update position
- const existentialDeposit = polkadotJs.consts.balances.existentialDeposit;
-
+ // We also need charlie to join staking because the settings for the dev environment are 1 collator for
+ // tanssi and 2 collators for containers, so we need 3 collators for bob to be assigned.
await context.createBlock([
+ // Remove all invulnerables, otherwise they have priority
await polkadotJs.tx.sudo
- .sudo(polkadotJs.tx.invulnerables.removeInvulnerable(alice.address))
- .signAsync(context.keyring.alice, { nonce: aliceNonce++ }),
- await polkadotJs.tx.sudo
- .sudo(polkadotJs.tx.invulnerables.removeInvulnerable(bob.address))
+ .sudo(polkadotJs.tx.invulnerables.setInvulnerables([]))
.signAsync(context.keyring.alice, { nonce: aliceNonce++ }),
await polkadotJs.tx.pooledStaking
.requestDelegate(alice.address, "ManualRewards", 10000n * DANCE)
@@ -42,9 +41,9 @@ describeSuite({
await polkadotJs.tx.pooledStaking
.requestDelegate(bob.address, "ManualRewards", 10000n * DANCE)
.signAsync(context.keyring.bob, { nonce: bobNonce++ }),
- await polkadotJs.tx.balances
- .transferAllowDeath(STAKING_ACCOUNT, existentialDeposit)
- .signAsync(context.keyring.bob, { nonce: bobNonce++ }),
+ await polkadotJs.tx.pooledStaking
+ .requestDelegate(charlie.address, "ManualRewards", 10000n * DANCE)
+ .signAsync(context.keyring.charlie, { nonce: 0 }),
]);
// At least 2 sessions for the change to have effect
await jumpSessions(context, 2);
diff --git a/test/suites/dev-tanssi/staking/test_staking_rewards_balanced.ts b/test/suites/dev-tanssi/staking/test_staking_rewards_balanced.ts
index ce2c1ac74..07d80f50b 100644
--- a/test/suites/dev-tanssi/staking/test_staking_rewards_balanced.ts
+++ b/test/suites/dev-tanssi/staking/test_staking_rewards_balanced.ts
@@ -9,7 +9,7 @@ import {
filterRewardStakingDelegators,
jumpSessions,
} from "util/block";
-import { DANCE, STAKING_ACCOUNT } from "util/constants";
+import { DANCE } from "util/constants";
describeSuite({
id: "DT0302",
@@ -32,15 +32,10 @@ describeSuite({
// we will make each of them delegate the other with 50%
// Alice autocompounding, Bob will be manual
- // Additionally, we need to pass to the staking account the minimum balance
- const existentialDeposit = polkadotJs.consts.balances.existentialDeposit;
-
await context.createBlock([
+ // Remove all invulnerables, otherwise they have priority
await polkadotJs.tx.sudo
- .sudo(polkadotJs.tx.invulnerables.removeInvulnerable(alice.address))
- .signAsync(context.keyring.alice, { nonce: aliceNonce++ }),
- await polkadotJs.tx.sudo
- .sudo(polkadotJs.tx.invulnerables.removeInvulnerable(bob.address))
+ .sudo(polkadotJs.tx.invulnerables.setInvulnerables([]))
.signAsync(context.keyring.alice, { nonce: aliceNonce++ }),
await polkadotJs.tx.pooledStaking
.requestDelegate(alice.address, "AutoCompounding", 10000n * DANCE)
@@ -54,9 +49,6 @@ describeSuite({
await polkadotJs.tx.pooledStaking
.requestDelegate(bob.address, "ManualRewards", 10000n * DANCE)
.signAsync(context.keyring.bob, { nonce: bobNonce++ }),
- await polkadotJs.tx.balances
- .transferAllowDeath(STAKING_ACCOUNT, existentialDeposit)
- .signAsync(context.keyring.bob, { nonce: bobNonce++ }),
]);
// At least 2 sessions for the change to have effect
await jumpSessions(context, 2);
diff --git a/test/suites/dev-tanssi/staking/test_staking_rewards_non_balanced.ts b/test/suites/dev-tanssi/staking/test_staking_rewards_non_balanced.ts
index b2b2174b3..7b57ee5c5 100644
--- a/test/suites/dev-tanssi/staking/test_staking_rewards_non_balanced.ts
+++ b/test/suites/dev-tanssi/staking/test_staking_rewards_non_balanced.ts
@@ -9,7 +9,7 @@ import {
filterRewardStakingDelegators,
jumpSessions,
} from "util/block";
-import { DANCE, STAKING_ACCOUNT } from "util/constants";
+import { DANCE } from "util/constants";
describeSuite({
id: "DT0303",
@@ -32,15 +32,10 @@ describeSuite({
// we will make each of them delegate the other with 50%
// Alice autocompounding, Bob will be manual
- // Additionally, we need to pass to the staking account the minimum balance
- const existentialDeposit = polkadotJs.consts.balances.existentialDeposit;
-
await context.createBlock([
+ // Remove all invulnerables, otherwise they have priority
await polkadotJs.tx.sudo
- .sudo(polkadotJs.tx.invulnerables.removeInvulnerable(alice.address))
- .signAsync(context.keyring.alice, { nonce: aliceNonce++ }),
- await polkadotJs.tx.sudo
- .sudo(polkadotJs.tx.invulnerables.removeInvulnerable(bob.address))
+ .sudo(polkadotJs.tx.invulnerables.setInvulnerables([]))
.signAsync(context.keyring.alice, { nonce: aliceNonce++ }),
await polkadotJs.tx.pooledStaking
.requestDelegate(alice.address, "AutoCompounding", 18000n * DANCE)
@@ -54,9 +49,6 @@ describeSuite({
await polkadotJs.tx.pooledStaking
.requestDelegate(bob.address, "ManualRewards", 2000n * DANCE)
.signAsync(context.keyring.bob, { nonce: bobNonce++ }),
- await polkadotJs.tx.balances
- .transferAllowDeath(STAKING_ACCOUNT, existentialDeposit)
- .signAsync(context.keyring.bob, { nonce: bobNonce++ }),
]);
// At least 2 sessions for the change to have effect
await jumpSessions(context, 2);