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
//! # Inactivity Tracking Pallet
17
//!
18
//! This pallet tracks and stores the activity of container chain and orchestrator chain collators
19
//! for configurable number of sessions. It is used to determine if a collator is inactive
20
//! for that period of time.
21
//!
22
//! The tracking functionality can be enabled or disabled with root privileges.
23
//! By default, the tracking is enabled.
24
#![cfg_attr(not(feature = "std"), no_std)]
25

            
26
use {
27
    frame_support::{dispatch::DispatchResult, pallet_prelude::Weight},
28
    parity_scale_codec::{Decode, Encode},
29
    scale_info::TypeInfo,
30
    serde::{Deserialize, Serialize},
31
    sp_core::{MaxEncodedLen, RuntimeDebug},
32
    sp_runtime::{traits::Get, BoundedBTreeSet},
33
    sp_staking::SessionIndex,
34
    tp_traits::{
35
        AuthorNotingHook, AuthorNotingInfo, ForSession, GetContainerChainsWithCollators,
36
        GetSessionIndex, MaybeSelfChainBlockAuthor, NodeActivityTrackingHelper, ParaId,
37
        ParathreadHelper,
38
    },
39
};
40

            
41
#[cfg(test)]
42
mod mock;
43

            
44
#[cfg(test)]
45
mod tests;
46

            
47
#[cfg(feature = "runtime-benchmarks")]
48
mod benchmarking;
49

            
50
pub mod weights;
51
pub use weights::WeightInfo;
52

            
53
#[cfg(feature = "runtime-benchmarks")]
54
use tp_traits::BlockNumber;
55

            
56
pub use pallet::*;
57
7416
#[frame_support::pallet]
58
pub mod pallet {
59
    use {
60
        super::*,
61
        crate::weights::WeightInfo,
62
        core::marker::PhantomData,
63
        frame_support::{pallet_prelude::*, storage::types::StorageMap},
64
        frame_system::pallet_prelude::*,
65
        sp_std::collections::btree_set::BTreeSet,
66
    };
67

            
68
    /// The status of collator activity tracking
69
    #[derive(
70
        Clone,
71
        PartialEq,
72
        Eq,
73
        Encode,
74
        DecodeWithMemTracking,
75
        Decode,
76
3708
        TypeInfo,
77
        Serialize,
78
        Deserialize,
79
        RuntimeDebug,
80
        MaxEncodedLen,
81
    )]
82
    pub enum ActivityTrackingStatus {
83
        Enabled {
84
            /// The session in which we will start recording the collator activity after enabling it
85
            start: SessionIndex,
86
            /// The session after which the activity tracking can be disabled
87
            end: SessionIndex,
88
        },
89
        Disabled {
90
            /// The session after which the activity tracking can be enabled
91
            end: SessionIndex,
92
        },
93
    }
94
    impl Default for ActivityTrackingStatus {
95
170046
        fn default() -> Self {
96
170046
            ActivityTrackingStatus::Enabled { start: 0, end: 0 }
97
170046
        }
98
    }
99

            
100
2640
    #[pallet::pallet]
101
    pub struct Pallet<T>(PhantomData<T>);
102
    #[pallet::config]
103
    pub trait Config: frame_system::Config {
104
        /// Overarching event type.
105
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
106

            
107
        /// A stable identifier for a collator.
108
        type CollatorId: Member
109
            + Parameter
110
            + Ord
111
            + MaybeSerializeDeserialize
112
            + MaxEncodedLen
113
            + TryFrom<Self::AccountId>;
114

            
115
        /// The maximum number of sessions for which a collator can be inactive
116
        /// before being moved to the offline queue
117
        #[pallet::constant]
118
        type MaxInactiveSessions: Get<u32>;
119

            
120
        /// The maximum amount of collators that can be stored for a session
121
        #[pallet::constant]
122
        type MaxCollatorsPerSession: Get<u32>;
123

            
124
        /// The maximum amount of container chains that can be stored
125
        #[pallet::constant]
126
        type MaxContainerChains: Get<u32>;
127

            
128
        /// Helper that returns the current session index.
129
        type CurrentSessionIndex: GetSessionIndex<SessionIndex>;
130

            
131
        /// Helper that fetches a list of collators eligible to produce blocks for the current session
132
        type CurrentCollatorsFetcher: GetContainerChainsWithCollators<Self::CollatorId>;
133

            
134
        /// Helper that returns the block author for the orchestrator chain (if it exists)
135
        type GetSelfChainBlockAuthor: MaybeSelfChainBlockAuthor<Self::CollatorId>;
136

            
137
        /// Helper that checks if a ParaId is a parathread
138
        type ParaFilter: ParathreadHelper;
139

            
140
        /// The weight information of this pallet.
141
        type WeightInfo: weights::WeightInfo;
142
    }
143

            
144
    /// Switch to enable/disable activity tracking
145
109304
    #[pallet::storage]
146
    pub type CurrentActivityTrackingStatus<T: Config> =
147
        StorageValue<_, ActivityTrackingStatus, ValueQuery>;
148

            
149
    /// A storage map of inactive collators for a session
150
5231
    #[pallet::storage]
151
    pub type InactiveCollators<T: Config> = StorageMap<
152
        _,
153
        Twox64Concat,
154
        SessionIndex,
155
        BoundedBTreeSet<T::CollatorId, T::MaxCollatorsPerSession>,
156
        ValueQuery,
157
    >;
158

            
159
    /// A list of active collators for a session. Repopulated at the start of every session
160
220156
    #[pallet::storage]
161
    pub type ActiveCollatorsForCurrentSession<T: Config> =
162
        StorageValue<_, BoundedBTreeSet<T::CollatorId, T::MaxCollatorsPerSession>, ValueQuery>;
163

            
164
    /// A list of active container chains for a session. Repopulated at the start of every session
165
104938
    #[pallet::storage]
166
    pub type ActiveContainerChainsForCurrentSession<T: Config> =
167
        StorageValue<_, BoundedBTreeSet<ParaId, T::MaxContainerChains>, ValueQuery>;
168

            
169
618
    #[pallet::event]
170
17
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
171
    pub enum Event<T: Config> {
172
        /// Event emitted when the activity tracking status is updated
173
        ActivityTrackingStatusSet { status: ActivityTrackingStatus },
174
    }
175

            
176
618
    #[pallet::error]
177
    pub enum Error<T> {
178
        /// The size of a collator set for a session has already reached MaxCollatorsPerSession value
179
        MaxCollatorsPerSessionReached,
180
        /// The size of a chains set for a session has already reached MaxContainerChains value
181
        MaxContainerChainsReached,
182
        /// Error returned when the activity tracking status is attempted to be updated before the end session
183
        ActivityTrackingStatusUpdateSuspended,
184
        /// Error returned when the activity tracking status is attempted to be enabled when it is already enabled
185
        ActivityTrackingStatusAlreadyEnabled,
186
        /// Error returned when the activity tracking status is attempted to be disabled when it is already disabled
187
        ActivityTrackingStatusAlreadyDisabled,
188
    }
189

            
190
618
    #[pallet::call]
191
    impl<T: Config> Pallet<T> {
192
        #[pallet::call_index(0)]
193
        #[pallet::weight(T::WeightInfo::set_inactivity_tracking_status())]
194
        pub fn set_inactivity_tracking_status(
195
            origin: OriginFor<T>,
196
            enable_inactivity_tracking: bool,
197
18
        ) -> DispatchResult {
198
18
            ensure_root(origin)?;
199
17
            let current_status_end_session_index = match <CurrentActivityTrackingStatus<T>>::get() {
200
11
                ActivityTrackingStatus::Enabled { start: _, end } => {
201
11
                    ensure!(
202
11
                        !enable_inactivity_tracking,
203
1
                        Error::<T>::ActivityTrackingStatusAlreadyEnabled
204
                    );
205
10
                    end
206
                }
207
6
                ActivityTrackingStatus::Disabled { end } => {
208
6
                    ensure!(
209
6
                        enable_inactivity_tracking,
210
1
                        Error::<T>::ActivityTrackingStatusAlreadyDisabled
211
                    );
212
5
                    end
213
                }
214
            };
215
15
            let current_session_index = T::CurrentSessionIndex::session_index();
216
15
            ensure!(
217
15
                current_session_index > current_status_end_session_index,
218
1
                Error::<T>::ActivityTrackingStatusUpdateSuspended
219
            );
220
14
            Self::set_inactivity_tracking_status_inner(
221
14
                current_session_index,
222
14
                enable_inactivity_tracking,
223
14
            );
224
14
            Ok(())
225
        }
226
    }
227

            
228
65949
    #[pallet::hooks]
229
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
230
29717
        fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
231
29717
            let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
232
            // Process the orchestrator chain block author (if it exists) and activity tracking is enabled
233
29717
            if let Some(orchestrator_chain_author) = T::GetSelfChainBlockAuthor::get_block_author()
234
            {
235
25823
                total_weight.saturating_accrue(T::DbWeight::get().reads(1));
236
25823
                if let ActivityTrackingStatus::Enabled { start, end: _ } =
237
25823
                    <CurrentActivityTrackingStatus<T>>::get()
238
                {
239
25823
                    total_weight.saturating_accrue(T::DbWeight::get().reads(1));
240
25823
                    if start <= T::CurrentSessionIndex::session_index() {
241
25823
                        total_weight
242
25823
                            .saturating_accrue(Self::on_author_noted(orchestrator_chain_author));
243
25823
                    }
244
                }
245
3894
            }
246
29717
            total_weight
247
29717
        }
248
    }
249

            
250
    impl<T: Config> Pallet<T> {
251
        /// Internal function to set the activity tracking status and
252
        /// clear ActiveCollatorsForCurrentSession if disabled
253
17
        fn set_inactivity_tracking_status_inner(
254
17
            current_session_index: SessionIndex,
255
17
            enable_inactivity_tracking: bool,
256
17
        ) {
257
17
            let new_status_end_session_index =
258
17
                current_session_index.saturating_add(T::MaxInactiveSessions::get());
259
17
            let new_status = if enable_inactivity_tracking {
260
5
                ActivityTrackingStatus::Enabled {
261
5
                    start: current_session_index.saturating_add(1),
262
5
                    end: new_status_end_session_index,
263
5
                }
264
            } else {
265
12
                <ActiveCollatorsForCurrentSession<T>>::put(BoundedBTreeSet::new());
266
12
                ActivityTrackingStatus::Disabled {
267
12
                    end: new_status_end_session_index,
268
12
                }
269
            };
270
17
            <CurrentActivityTrackingStatus<T>>::put(new_status.clone());
271
17
            Self::deposit_event(Event::<T>::ActivityTrackingStatusSet { status: new_status })
272
17
        }
273

            
274
        /// Internal function to clear the active collators for the current session
275
        /// and remove the collators records that are outside the activity period.
276
        /// Triggered at the beginning of each session.
277
3982
        pub fn process_ended_session() {
278
3982
            let current_session_index = T::CurrentSessionIndex::session_index();
279
3982
            <ActiveCollatorsForCurrentSession<T>>::put(BoundedBTreeSet::new());
280
3982
            <ActiveContainerChainsForCurrentSession<T>>::put(BoundedBTreeSet::new());
281
3982

            
282
3982
            // Cleanup active collator info for sessions that are older than the maximum allowed
283
3982
            if current_session_index > T::MaxInactiveSessions::get() {
284
1529
                <crate::pallet::InactiveCollators<T>>::remove(
285
1529
                    current_session_index
286
1529
                        .saturating_sub(T::MaxInactiveSessions::get())
287
1529
                        .saturating_sub(1),
288
1529
                );
289
3017
            }
290
3982
        }
291

            
292
        /// Internal function to populate the inactivity tracking storage used for marking collator
293
        /// as inactive. Triggered at the end of a session.
294
2998
        pub fn on_before_session_ending() {
295
2998
            let current_session_index = T::CurrentSessionIndex::session_index();
296
2998
            Self::process_inactive_chains_for_session();
297
2998
            match <CurrentActivityTrackingStatus<T>>::get() {
298
16
                ActivityTrackingStatus::Disabled { .. } => return,
299
2982
                ActivityTrackingStatus::Enabled { start, end: _ } => {
300
2982
                    if start > current_session_index {
301
5
                        return;
302
2977
                    }
303
                }
304
            }
305
2977
            if let Ok(inactive_collators) =
306
2977
                BoundedBTreeSet::<T::CollatorId, T::MaxCollatorsPerSession>::try_from(
307
2977
                    T::CurrentCollatorsFetcher::get_all_collators_assigned_to_chains(
308
2977
                        ForSession::Current,
309
2977
                    )
310
2977
                    .difference(&<ActiveCollatorsForCurrentSession<T>>::get())
311
2977
                    .cloned()
312
2977
                    .collect::<BTreeSet<T::CollatorId>>(),
313
2977
                )
314
2977
            {
315
2977
                InactiveCollators::<T>::insert(current_session_index, inactive_collators);
316
2977
            } else {
317
                // If we reach MaxCollatorsPerSession limit there must be a bug in the pallet
318
                // so we disable the activity tracking
319
                Self::set_inactivity_tracking_status_inner(current_session_index, false);
320
            }
321
2998
        }
322

            
323
        /// Internal function to populate the current session active collator records with collators
324
        /// part of inactive chains.
325
2998
        pub fn process_inactive_chains_for_session() {
326
2998
            match <CurrentActivityTrackingStatus<T>>::get() {
327
15
                ActivityTrackingStatus::Disabled { .. } => return,
328
2983
                ActivityTrackingStatus::Enabled { start, end: _ } => {
329
2983
                    if start > T::CurrentSessionIndex::session_index() {
330
5
                        return;
331
2978
                    }
332
2978
                }
333
2978
            }
334
2978
            let mut active_chains = <ActiveContainerChainsForCurrentSession<T>>::get().into_inner();
335
2978
            // Removing the parathreads for the current session from the active chains array.
336
2978
            // In this way we handle all parathreads as inactive chains.
337
2978
            // This solution would only work if a collator either:
338
2978
            // - is assigned to one chain only
339
2978
            // - is assigned to multiple chains but all of them are parathreads
340
2978
            active_chains = active_chains
341
2978
                .difference(&T::ParaFilter::get_parathreads_for_session())
342
2978
                .cloned()
343
2978
                .collect::<BTreeSet<ParaId>>();
344
2978

            
345
2978
            let _ = <ActiveCollatorsForCurrentSession<T>>::try_mutate(
346
2978
                |active_collators| -> DispatchResult {
347
2978
                    let container_chains_with_collators =
348
2978
                        T::CurrentCollatorsFetcher::container_chains_with_collators(
349
2978
                            ForSession::Current,
350
2978
                        );
351

            
352
5067
                    for (para_id, collator_ids) in container_chains_with_collators.iter() {
353
4854
                        if !active_chains.contains(para_id) {
354
                            // Collators assigned to inactive chain are added
355
                            // to the current active collators storage
356
3618
                            for collator_id in collator_ids {
357
878
                                if let Err(_) = active_collators.try_insert(collator_id.clone()) {
358
                                    // If we reach MaxCollatorsPerSession limit there must be a bug in the pallet
359
                                    // so we disable the activity tracking
360
1
                                    Self::set_inactivity_tracking_status_inner(
361
1
                                        T::CurrentSessionIndex::session_index(),
362
1
                                        false,
363
1
                                    );
364
1
                                    return Err(Error::<T>::MaxCollatorsPerSessionReached.into());
365
877
                                }
366
                            }
367
2113
                        }
368
                    }
369
2977
                    Ok(())
370
2978
                },
371
2978
            );
372
2998
        }
373

            
374
        /// Internal update the current session active collator records.
375
        /// This function is called when a container chain or orchestrator chain collator is noted.
376
48391
        pub fn on_author_noted(author: T::CollatorId) -> Weight {
377
48391
            let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
378
48391
            let _ = <ActiveCollatorsForCurrentSession<T>>::try_mutate(
379
48391
                |active_collators| -> DispatchResult {
380
48391
                    if let Err(_) = active_collators.try_insert(author.clone()) {
381
                        // If we reach MaxCollatorsPerSession limit there must be a bug in the pallet
382
                        // so we disable the activity tracking
383
1
                        Self::set_inactivity_tracking_status_inner(
384
1
                            T::CurrentSessionIndex::session_index(),
385
1
                            false,
386
1
                        );
387
1
                        total_weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2));
388
1
                        return Err(Error::<T>::MaxCollatorsPerSessionReached.into());
389
48390
                    } else {
390
48390
                        total_weight.saturating_accrue(T::DbWeight::get().writes(1));
391
48390
                    }
392
48390
                    Ok(())
393
48391
                },
394
48391
            );
395
48391
            total_weight
396
48391
        }
397

            
398
        /// Internal update the current session active chains records.
399
        /// This function is called when a container chain is noted.
400
22568
        pub fn on_chain_noted(chain_id: ParaId) -> Weight {
401
22568
            let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
402
22568
            let _ = <ActiveContainerChainsForCurrentSession<T>>::try_mutate(
403
22568
                |active_chains| -> DispatchResult {
404
22568
                    if let Err(_) = active_chains.try_insert(chain_id) {
405
                        // If we reach MaxContainerChains limit there must be a bug in the pallet
406
                        // so we disable the activity tracking
407
1
                        Self::set_inactivity_tracking_status_inner(
408
1
                            T::CurrentSessionIndex::session_index(),
409
1
                            false,
410
1
                        );
411
1
                        total_weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2));
412
1
                        return Err(Error::<T>::MaxContainerChainsReached.into());
413
22567
                    } else {
414
22567
                        total_weight += T::DbWeight::get().writes(1);
415
22567
                    }
416
22567
                    Ok(())
417
22568
                },
418
22568
            );
419
22568
            total_weight
420
22568
        }
421
    }
422
}
423

            
424
impl<T: Config> NodeActivityTrackingHelper<T::CollatorId> for Pallet<T> {
425
41
    fn is_node_inactive(node: &T::CollatorId) -> bool {
426
41
        // If inactivity tracking is not enabled all nodes are considered active.
427
41
        // We don't need to check the activity records and can return false
428
41
        // Inactivity tracking is not enabled if
429
41
        // - the status is disabled
430
41
        // - the CurrentSessionIndex < start session + MaxInactiveSessions index since there won't be
431
41
        // sufficient activity records to determine inactivity
432
41
        let current_session_index = T::CurrentSessionIndex::session_index();
433
41
        let minimum_sessions_required = T::MaxInactiveSessions::get();
434
41
        match <CurrentActivityTrackingStatus<T>>::get() {
435
2
            ActivityTrackingStatus::Disabled { .. } => return false,
436
39
            ActivityTrackingStatus::Enabled { start, end: _ } => {
437
39
                if start.saturating_add(minimum_sessions_required) > current_session_index {
438
16
                    return false;
439
23
                }
440
23
            }
441
23
        }
442
23

            
443
23
        let start_session_index = current_session_index.saturating_sub(minimum_sessions_required);
444
66
        for session_index in start_session_index..current_session_index {
445
66
            if !<InactiveCollators<T>>::get(session_index).contains(node) {
446
11
                return false;
447
55
            }
448
        }
449
12
        true
450
41
    }
451
}
452

            
453
impl<T: Config> AuthorNotingHook<T::CollatorId> for Pallet<T> {
454
22432
    fn on_container_authors_noted(info: &[AuthorNotingInfo<T::CollatorId>]) -> Weight {
455
22432
        let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
456
22430
        if let ActivityTrackingStatus::Enabled { start, end: _ } =
457
22432
            <CurrentActivityTrackingStatus<T>>::get()
458
        {
459
22430
            if start <= T::CurrentSessionIndex::session_index() {
460
44996
                for author_info in info {
461
22568
                    total_weight
462
22568
                        .saturating_accrue(Self::on_author_noted(author_info.author.clone()));
463
22568
                    total_weight.saturating_accrue(Self::on_chain_noted(author_info.para_id));
464
22568
                }
465
2
            }
466
2
        }
467
22432
        total_weight
468
22432
    }
469
    #[cfg(feature = "runtime-benchmarks")]
470
    fn prepare_worst_case_for_bench(_a: &T::CollatorId, _b: BlockNumber, _para_id: ParaId) {}
471
}