1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! # Collator Assignment Pallet
18
//!
19
//! This pallet assigns a list of collators to:
20
//!    - the orchestrator chain
21
//!    - a set of container chains
22
//!
23
//! The set of container chains is retrieved thanks to the GetContainerChains trait
24
//! The number of collators to assign to the orchestrator chain and the number
25
//! of collators to assign to each container chain is retrieved through the GetHostConfiguration
26
//! trait.
27
//!  
28
//! The pallet uses the following approach:
29
//!
30
//! - First, it aims at filling the necessary collators to serve the orchestrator chain
31
//! - Second, it aims at filling in-order (FIFO) the existing containerChains
32
//!
33
//! Upon new session, this pallet takes whatever assignation was in the PendingCollatorContainerChain
34
//! storage, and assigns it as the current CollatorContainerChain. In addition, it takes the next
35
//! queued set of parachains and collators and calculates the assignment for the next session, storing
36
//! it in the PendingCollatorContainerChain storage item.
37
//!
38
//! The reason for the collator-assignment pallet to work with a one-session delay assignment is because
39
//! we want collators to know at least one session in advance the container chain/orchestrator that they
40
//! are assigned to.
41

            
42
#![cfg_attr(not(feature = "std"), no_std)]
43

            
44
use {
45
    crate::assignment::{Assignment, ChainNumCollators},
46
    frame_support::{pallet_prelude::*, traits::Currency},
47
    frame_system::pallet_prelude::BlockNumberFor,
48
    rand::{seq::SliceRandom, SeedableRng},
49
    rand_chacha::ChaCha20Rng,
50
    sp_runtime::{
51
        traits::{AtLeast32BitUnsigned, One, Zero},
52
        Saturating,
53
    },
54
    sp_std::{collections::btree_set::BTreeSet, fmt::Debug, prelude::*, vec},
55
    tp_traits::{
56
        CollatorAssignmentHook, CollatorAssignmentTip, GetContainerChainAuthor,
57
        GetHostConfiguration, GetSessionContainerChains, ParaId, RemoveInvulnerables,
58
        RemoveParaIdsWithNoCredits, ShouldRotateAllCollators, Slot,
59
    },
60
};
61
pub use {dp_collator_assignment::AssignedCollators, pallet::*};
62

            
63
mod assignment;
64
#[cfg(feature = "runtime-benchmarks")]
65
mod benchmarking;
66
pub mod weights;
67
pub use weights::WeightInfo;
68

            
69
#[cfg(test)]
70
mod mock;
71

            
72
#[cfg(test)]
73
mod tests;
74

            
75
1584
#[frame_support::pallet]
76
pub mod pallet {
77
    use super::*;
78

            
79
182
    #[pallet::pallet]
80
    #[pallet::without_storage_info]
81
    pub struct Pallet<T>(_);
82

            
83
    /// Configure the pallet by specifying the parameters and types on which it depends.
84
    #[pallet::config]
85
    pub trait Config: frame_system::Config {
86
        /// The overarching event type.
87
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
88
        type SessionIndex: parity_scale_codec::FullCodec
89
            + TypeInfo
90
            + Copy
91
            + AtLeast32BitUnsigned
92
            + Debug;
93
        // `SESSION_DELAY` is used to delay any changes to Paras registration or configurations.
94
        // Wait until the session index is 2 larger then the current index to apply any changes,
95
        // which guarantees that at least one full session has passed before any changes are applied.
96
        type HostConfiguration: GetHostConfiguration<Self::SessionIndex>;
97
        type ContainerChains: GetSessionContainerChains<Self::SessionIndex>;
98
        type SelfParaId: Get<ParaId>;
99
        type ShouldRotateAllCollators: ShouldRotateAllCollators<Self::SessionIndex>;
100
        type GetRandomnessForNextBlock: GetRandomnessForNextBlock<BlockNumberFor<Self>>;
101
        type RemoveInvulnerables: RemoveInvulnerables<Self::AccountId>;
102
        type RemoveParaIdsWithNoCredits: RemoveParaIdsWithNoCredits;
103
        type CollatorAssignmentHook: CollatorAssignmentHook<BalanceOf<Self>>;
104
        type Currency: Currency<Self::AccountId>;
105
        type CollatorAssignmentTip: CollatorAssignmentTip<BalanceOf<Self>>;
106
        /// The weight information of this pallet.
107
        type WeightInfo: WeightInfo;
108
    }
109

            
110
    #[pallet::event]
111
1863
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
112
    pub enum Event<T: Config> {
113
11
        NewPendingAssignment {
114
            random_seed: [u8; 32],
115
            full_rotation: bool,
116
            target_session: T::SessionIndex,
117
        },
118
    }
119

            
120
58664
    #[pallet::storage]
121
    pub(crate) type CollatorContainerChain<T: Config> =
122
        StorageValue<_, AssignedCollators<T::AccountId>, ValueQuery>;
123

            
124
    /// Pending configuration changes.
125
    ///
126
    /// This is a list of configuration changes, each with a session index at which it should
127
    /// be applied.
128
    ///
129
    /// The list is sorted ascending by session index. Also, this list can only contain at most
130
    /// 2 items: for the next session and for the `scheduled_session`.
131
9880
    #[pallet::storage]
132
    pub(crate) type PendingCollatorContainerChain<T: Config> =
133
        StorageValue<_, Option<AssignedCollators<T::AccountId>>, ValueQuery>;
134

            
135
    /// Randomness from previous block. Used to shuffle collators on session change.
136
    /// Should only be set on the last block of each session and should be killed on the on_initialize of the next block.
137
    /// The default value of [0; 32] disables randomness in the pallet.
138
5656
    #[pallet::storage]
139
    pub(crate) type Randomness<T: Config> = StorageValue<_, [u8; 32], ValueQuery>;
140

            
141
176
    #[pallet::call]
142
    impl<T: Config> Pallet<T> {}
143

            
144
    /// A struct that holds the assignment that is active after the session change and optionally
145
    /// the assignment that becomes active after the next session change.
146
    pub struct SessionChangeOutcome<T: Config> {
147
        /// New active assignment.
148
        pub active_assignment: AssignedCollators<T::AccountId>,
149
        /// Next session active assignment.
150
        pub next_assignment: AssignedCollators<T::AccountId>,
151
        /// Total number of registered parachains before filtering them out, used as a weight hint
152
        pub num_total_registered_paras: u32,
153
    }
154

            
155
    impl<T: Config> Pallet<T> {
156
        /// Assign new collators
157
        /// collators should be queued collators
158
1863
        pub fn assign_collators(
159
1863
            current_session_index: &T::SessionIndex,
160
1863
            random_seed: [u8; 32],
161
1863
            collators: Vec<T::AccountId>,
162
1863
        ) -> SessionChangeOutcome<T> {
163
1863
            // We work with one session delay to calculate assignments
164
1863
            let session_delay = T::SessionIndex::one();
165
1863
            let target_session_index = current_session_index.saturating_add(session_delay);
166
1863
            // We get the containerChains that we will have at the target session
167
1863
            let container_chains =
168
1863
                T::ContainerChains::session_container_chains(target_session_index);
169
1863
            let num_total_registered_paras =
170
1863
                (container_chains.parachains.len() + container_chains.parathreads.len()) as u32;
171
1863
            let mut container_chain_ids = container_chains.parachains;
172
1863
            let mut parathreads: Vec<_> = container_chains
173
1863
                .parathreads
174
1863
                .into_iter()
175
1863
                .map(|(para_id, _)| para_id)
176
1863
                .collect();
177
1863

            
178
1863
            // We read current assigned collators
179
1863
            let old_assigned = Self::read_assigned_collators();
180
1863
            let old_assigned_para_ids: BTreeSet<ParaId> =
181
1863
                old_assigned.container_chains.keys().cloned().collect();
182
1863

            
183
1863
            // Remove the containerChains that do not have enough credits for block production
184
1863
            T::RemoveParaIdsWithNoCredits::remove_para_ids_with_no_credits(
185
1863
                &mut container_chain_ids,
186
1863
                &old_assigned_para_ids,
187
1863
            );
188
1863
            // TODO: parathreads should be treated a bit differently, they don't need to have the same amount of credits
189
1863
            // as parathreads because they will not be producing blocks on every slot.
190
1863
            T::RemoveParaIdsWithNoCredits::remove_para_ids_with_no_credits(
191
1863
                &mut parathreads,
192
1863
                &old_assigned_para_ids,
193
1863
            );
194
1863

            
195
1863
            let mut shuffle_collators = None;
196
1863
            // If the random_seed is all zeros, we don't shuffle the list of collators nor the list
197
1863
            // of container chains.
198
1863
            // This should only happen in tests, and in the genesis block.
199
1863
            if random_seed != [0; 32] {
200
74
                let mut rng: ChaCha20Rng = SeedableRng::from_seed(random_seed);
201
74
                container_chain_ids.shuffle(&mut rng);
202
74
                parathreads.shuffle(&mut rng);
203
74
                shuffle_collators = Some(move |collators: &mut Vec<T::AccountId>| {
204
74
                    collators.shuffle(&mut rng);
205
74
                })
206
1789
            }
207

            
208
1863
            let orchestrator_chain = ChainNumCollators {
209
1863
                para_id: T::SelfParaId::get(),
210
1863
                min_collators: T::HostConfiguration::min_collators_for_orchestrator(
211
1863
                    target_session_index,
212
1863
                ),
213
1863
                max_collators: T::HostConfiguration::max_collators_for_orchestrator(
214
1863
                    target_session_index,
215
1863
                ),
216
1863
            };
217
1863
            // Initialize list of chains as `[container1, container2, parathread1, parathread2]`.
218
1863
            // The order means priority: the first chain in the list will be the first one to get assigned collators.
219
1863
            // Chains will not be assigned less than `min_collators`, except the orchestrator chain.
220
1863
            // First all chains will be assigned `min_collators`, and then the first one will be assigned up to `max`,
221
1863
            // then the second one, and so on.
222
1863
            let mut chains = vec![];
223
1863
            let collators_per_container =
224
1863
                T::HostConfiguration::collators_per_container(target_session_index);
225
4412
            for para_id in &container_chain_ids {
226
2549
                chains.push(ChainNumCollators {
227
2549
                    para_id: *para_id,
228
2549
                    min_collators: collators_per_container,
229
2549
                    max_collators: collators_per_container,
230
2549
                });
231
2549
            }
232
1863
            let collators_per_parathread =
233
1863
                T::HostConfiguration::collators_per_parathread(target_session_index);
234
1943
            for para_id in &parathreads {
235
80
                chains.push(ChainNumCollators {
236
80
                    para_id: *para_id,
237
80
                    min_collators: collators_per_parathread,
238
80
                    max_collators: collators_per_parathread,
239
80
                });
240
80
            }
241

            
242
            // Are there enough collators to satisfy the minimum demand?
243
1863
            let enough_collators_for_all_chain = collators.len() as u32
244
1863
                >= T::HostConfiguration::min_collators_for_orchestrator(target_session_index)
245
1863
                    .saturating_add(
246
1863
                        collators_per_container.saturating_mul(container_chain_ids.len() as u32),
247
1863
                    )
248
1863
                    .saturating_add(
249
1863
                        collators_per_parathread.saturating_mul(parathreads.len() as u32),
250
1863
                    );
251
1863

            
252
1863
            // Prioritize paras by tip on congestion
253
1863
            // As of now this doesn't distinguish between parachains and parathreads
254
1863
            // TODO apply different logic to parathreads
255
1863
            if !enough_collators_for_all_chain {
256
1186
                chains.sort_by(|a, b| {
257
1184
                    T::CollatorAssignmentTip::get_para_tip(b.para_id)
258
1184
                        .cmp(&T::CollatorAssignmentTip::get_para_tip(a.para_id))
259
1186
                });
260
1196
            }
261

            
262
            // We assign new collators
263
            // we use the config scheduled at the target_session_index
264
1863
            let new_assigned =
265
1863
                if T::ShouldRotateAllCollators::should_rotate_all_collators(target_session_index) {
266
132
                    log::debug!(
267
                        "Collator assignment: rotating collators. Session {:?}, Seed: {:?}",
268
                        current_session_index.encode(),
269
                        random_seed
270
                    );
271

            
272
132
                    Self::deposit_event(Event::NewPendingAssignment {
273
132
                        random_seed,
274
132
                        full_rotation: true,
275
132
                        target_session: target_session_index,
276
132
                    });
277
132

            
278
132
                    Assignment::<T>::assign_collators_rotate_all(
279
132
                        collators,
280
132
                        orchestrator_chain,
281
132
                        chains,
282
132
                        shuffle_collators,
283
132
                    )
284
                } else {
285
1731
                    log::debug!(
286
                        "Collator assignment: keep old assigned. Session {:?}, Seed: {:?}",
287
                        current_session_index.encode(),
288
                        random_seed
289
                    );
290

            
291
1731
                    Self::deposit_event(Event::NewPendingAssignment {
292
1731
                        random_seed,
293
1731
                        full_rotation: false,
294
1731
                        target_session: target_session_index,
295
1731
                    });
296
1731

            
297
1731
                    Assignment::<T>::assign_collators_always_keep_old(
298
1731
                        collators,
299
1731
                        orchestrator_chain,
300
1731
                        chains,
301
1731
                        old_assigned.clone(),
302
1731
                        shuffle_collators,
303
1731
                    )
304
                };
305

            
306
1863
            let mut new_assigned = match new_assigned {
307
1859
                Ok(x) => x,
308
4
                Err(e) => {
309
4
                    log::error!(
310
4
                        "Error in collator assignment, will keep previous assignment. {:?}",
311
                        e
312
                    );
313

            
314
4
                    old_assigned.clone()
315
                }
316
            };
317

            
318
1863
            let mut assigned_containers = new_assigned.container_chains.clone();
319
2631
            assigned_containers.retain(|_, v| !v.is_empty());
320

            
321
            // On congestion, prioritized chains need to pay the minimum tip of the prioritized chains
322
1863
            let maybe_tip: Option<BalanceOf<T>> = if enough_collators_for_all_chain {
323
755
                None
324
            } else {
325
1108
                assigned_containers
326
1108
                    .into_keys()
327
1108
                    .filter_map(T::CollatorAssignmentTip::get_para_tip)
328
1108
                    .min()
329
            };
330

            
331
            // TODO: this probably is asking for a refactor
332
            // only apply the onCollatorAssignedHook if sufficient collators
333
4412
            for para_id in &container_chain_ids {
334
2549
                if !new_assigned
335
2549
                    .container_chains
336
2549
                    .get(para_id)
337
2549
                    .unwrap_or(&vec![])
338
2549
                    .is_empty()
339
                {
340
1232
                    if let Err(e) = T::CollatorAssignmentHook::on_collators_assigned(
341
1232
                        *para_id,
342
1232
                        maybe_tip.as_ref(),
343
1232
                        false,
344
1232
                    ) {
345
                        // On error remove para from assignment
346
2
                        log::warn!(
347
                            "CollatorAssignmentHook error! Removing para {} from assignment: {:?}",
348
                            u32::from(*para_id),
349
                            e
350
                        );
351
2
                        new_assigned.container_chains.remove(para_id);
352
1230
                    }
353
1317
                }
354
            }
355

            
356
1943
            for para_id in &parathreads {
357
80
                if !new_assigned
358
80
                    .container_chains
359
80
                    .get(para_id)
360
80
                    .unwrap_or(&vec![])
361
80
                    .is_empty()
362
                {
363
69
                    if let Err(e) = T::CollatorAssignmentHook::on_collators_assigned(
364
69
                        *para_id,
365
69
                        maybe_tip.as_ref(),
366
69
                        true,
367
69
                    ) {
368
                        // On error remove para from assignment
369
                        log::warn!(
370
                            "CollatorAssignmentHook error! Removing para {} from assignment: {:?}",
371
                            u32::from(*para_id),
372
                            e
373
                        );
374
                        new_assigned.container_chains.remove(para_id);
375
69
                    }
376
11
                }
377
            }
378

            
379
1863
            let mut pending = PendingCollatorContainerChain::<T>::get();
380
1863

            
381
1863
            let old_assigned_changed = old_assigned != new_assigned;
382
1863
            let mut pending_changed = false;
383
            // Update CollatorContainerChain using last entry of pending, if needed
384
1863
            if let Some(current) = pending.take() {
385
605
                pending_changed = true;
386
605
                CollatorContainerChain::<T>::put(current);
387
1277
            }
388
1863
            if old_assigned_changed {
389
835
                pending = Some(new_assigned.clone());
390
835
                pending_changed = true;
391
1323
            }
392
            // Update PendingCollatorContainerChain, if it changed
393
1863
            if pending_changed {
394
1180
                PendingCollatorContainerChain::<T>::put(pending);
395
1464
            }
396

            
397
            // Only applies to session index 0
398
1863
            if current_session_index == &T::SessionIndex::zero() {
399
508
                CollatorContainerChain::<T>::put(new_assigned.clone());
400
508
                return SessionChangeOutcome {
401
508
                    active_assignment: new_assigned.clone(),
402
508
                    next_assignment: new_assigned,
403
508
                    num_total_registered_paras,
404
508
                };
405
1355
            }
406
1355

            
407
1355
            SessionChangeOutcome {
408
1355
                active_assignment: old_assigned,
409
1355
                next_assignment: new_assigned,
410
1355
                num_total_registered_paras,
411
1355
            }
412
1863
        }
413

            
414
        // Returns the assigned collators as read from storage.
415
        // If there is any item in PendingCollatorContainerChain, returns that element.
416
        // Otherwise, reads and returns the current CollatorContainerChain
417
1863
        fn read_assigned_collators() -> AssignedCollators<T::AccountId> {
418
1863
            let mut pending_collator_list = PendingCollatorContainerChain::<T>::get();
419

            
420
1863
            if let Some(assigned_collators) = pending_collator_list.take() {
421
605
                assigned_collators
422
            } else {
423
                // Read current
424
1258
                CollatorContainerChain::<T>::get()
425
            }
426
1863
        }
427

            
428
1863
        pub fn initializer_on_new_session(
429
1863
            session_index: &T::SessionIndex,
430
1863
            collators: Vec<T::AccountId>,
431
1863
        ) -> SessionChangeOutcome<T> {
432
1863
            let random_seed = Randomness::<T>::take();
433
1863
            let num_collators = collators.len();
434
1863
            let assigned_collators = Self::assign_collators(session_index, random_seed, collators);
435
1863
            let num_total_registered_paras = assigned_collators.num_total_registered_paras;
436
1863

            
437
1863
            frame_system::Pallet::<T>::register_extra_weight_unchecked(
438
1863
                T::WeightInfo::new_session(num_collators as u32, num_total_registered_paras),
439
1863
                DispatchClass::Mandatory,
440
1863
            );
441
1863

            
442
1863
            assigned_collators
443
1863
        }
444

            
445
26881
        pub fn collator_container_chain() -> AssignedCollators<T::AccountId> {
446
26881
            CollatorContainerChain::<T>::get()
447
26881
        }
448

            
449
32
        pub fn pending_collator_container_chain() -> Option<AssignedCollators<T::AccountId>> {
450
32
            PendingCollatorContainerChain::<T>::get()
451
32
        }
452

            
453
        pub fn randomness() -> [u8; 32] {
454
            Randomness::<T>::get()
455
        }
456
    }
457

            
458
    impl<T: Config> GetContainerChainAuthor<T::AccountId> for Pallet<T> {
459
        // TODO: pending collator container chain if the block is a session change!
460
26662
        fn author_for_slot(slot: Slot, para_id: ParaId) -> Option<T::AccountId> {
461
26662
            let assigned_collators = Pallet::<T>::collator_container_chain();
462
26662
            let collators = if para_id == T::SelfParaId::get() {
463
13202
                Some(&assigned_collators.orchestrator_chain)
464
            } else {
465
13460
                assigned_collators.container_chains.get(&para_id)
466
754
            }?;
467

            
468
25908
            if collators.is_empty() {
469
                // Avoid division by zero below
470
6128
                return None;
471
19780
            }
472
19780
            let author_index = u64::from(slot) % collators.len() as u64;
473
19780
            collators.get(author_index as usize).cloned()
474
26662
        }
475

            
476
        #[cfg(feature = "runtime-benchmarks")]
477
        fn set_authors_for_para_id(para_id: ParaId, authors: Vec<T::AccountId>) {
478
            let mut assigned_collators = Pallet::<T>::collator_container_chain();
479
            assigned_collators.container_chains.insert(para_id, authors);
480
            CollatorContainerChain::<T>::put(assigned_collators);
481
        }
482
    }
483

            
484
19845
    #[pallet::hooks]
485
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
486
10607
        fn on_initialize(n: BlockNumberFor<T>) -> Weight {
487
10607
            let mut weight = Weight::zero();
488
10607

            
489
10607
            // Account reads and writes for on_finalize
490
10607
            if T::GetRandomnessForNextBlock::should_end_session(n.saturating_add(One::one())) {
491
968
                weight += T::DbWeight::get().reads_writes(1, 1);
492
9639
            }
493

            
494
10607
            weight
495
10607
        }
496

            
497
10440
        fn on_finalize(n: BlockNumberFor<T>) {
498
10440
            // If the next block is a session change, read randomness and store in pallet storage
499
10440
            if T::GetRandomnessForNextBlock::should_end_session(n.saturating_add(One::one())) {
500
965
                let random_seed = T::GetRandomnessForNextBlock::get_randomness();
501
965
                Randomness::<T>::put(random_seed);
502
9475
            }
503
10440
        }
504
    }
505
}
506

            
507
/// Balance used by this pallet
508
pub type BalanceOf<T> =
509
    <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
510

            
511
pub struct RotateCollatorsEveryNSessions<Period>(PhantomData<Period>);
512

            
513
impl<Period> ShouldRotateAllCollators<u32> for RotateCollatorsEveryNSessions<Period>
514
where
515
    Period: Get<u32>,
516
{
517
1540
    fn should_rotate_all_collators(session_index: u32) -> bool {
518
1540
        let period = Period::get();
519
1540

            
520
1540
        if period == 0 {
521
            // A period of 0 disables rotation
522
74
            false
523
        } else {
524
1466
            session_index % Period::get() == 0
525
        }
526
1540
    }
527
}
528

            
529
pub trait GetRandomnessForNextBlock<BlockNumber> {
530
    fn should_end_session(block_number: BlockNumber) -> bool;
531
    fn get_randomness() -> [u8; 32];
532
}
533

            
534
impl<BlockNumber> GetRandomnessForNextBlock<BlockNumber> for () {
535
2073
    fn should_end_session(_block_number: BlockNumber) -> bool {
536
2073
        false
537
2073
    }
538

            
539
    fn get_randomness() -> [u8; 32] {
540
        [0; 32]
541
    }
542
}