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, GetContainerChainsWithCollators, GetSessionIndex,
36
        MaybeSelfChainBlockAuthor, NodeActivityTrackingHelper,
37
    },
38
};
39

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

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

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

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

            
52
#[cfg(feature = "runtime-benchmarks")]
53
use tp_traits::{BlockNumber, ParaId};
54

            
55
pub use pallet::*;
56

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

            
69
    /// The status of collator activity tracking
70
    #[derive(
71
        Clone,
72
        PartialEq,
73
        Eq,
74
        Encode,
75
        Decode,
76
3672
        TypeInfo,
77
        Serialize,
78
        Deserialize,
79
        RuntimeDebug,
80
        MaxEncodedLen,
81
    )]
82
    pub enum ActivityTrackingStatus {
83
21
        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
21
        Disabled {
90
            /// The session after which the activity tracking can be enabled
91
            end: SessionIndex,
92
        },
93
    }
94
    impl Default for ActivityTrackingStatus {
95
149084
        fn default() -> Self {
96
149084
            ActivityTrackingStatus::Enabled { start: 0, end: 0 }
97
149084
        }
98
    }
99

            
100
2606
    #[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 stored for a session
121
        #[pallet::constant]
122
        type MaxCollatorsPerSession: Get<u32>;
123

            
124
        /// Helper that returns the current session index.
125
        type CurrentSessionIndex: GetSessionIndex<SessionIndex>;
126

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

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

            
133
        /// The weight information of this pallet.
134
        type WeightInfo: weights::WeightInfo;
135
    }
136

            
137
    /// Switch to enable/disable activity tracking
138
102680
    #[pallet::storage]
139
    pub type CurrentActivityTrackingStatus<T: Config> =
140
        StorageValue<_, ActivityTrackingStatus, ValueQuery>;
141

            
142
    /// A storage map of inactive collators for a session
143
5221
    #[pallet::storage]
144
    pub type InactiveCollators<T: Config> = StorageMap<
145
        _,
146
        Twox64Concat,
147
        SessionIndex,
148
        BoundedBTreeSet<T::CollatorId, T::MaxCollatorsPerSession>,
149
        ValueQuery,
150
    >;
151

            
152
    /// A list of active collators for a session. Repopulated at the start of every session
153
206904
    #[pallet::storage]
154
    pub type ActiveCollatorsForCurrentSession<T: Config> =
155
        StorageValue<_, BoundedBTreeSet<T::CollatorId, T::MaxCollatorsPerSession>, ValueQuery>;
156

            
157
612
    #[pallet::event]
158
11
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
159
    pub enum Event<T: Config> {
160
        /// Event emitted when the activity tracking status is updated
161
        ActivityTrackingStatusSet { status: ActivityTrackingStatus },
162
    }
163

            
164
14
    #[pallet::error]
165
    pub enum Error<T> {
166
        /// The size of a collator set for a session has already reached MaxCollatorsPerSession value
167
        MaxCollatorsPerSessionReached,
168
        /// Error returned when the activity tracking status is attempted to be updated before the end session
169
        ActivityTrackingStatusUpdateSuspended,
170
        /// Error returned when the activity tracking status is attempted to be enabled when it is already enabled
171
        ActivityTrackingStatusAlreadyEnabled,
172
        /// Error returned when the activity tracking status is attempted to be disabled when it is already disabled
173
        ActivityTrackingStatusAlreadyDisabled,
174
    }
175

            
176
    #[pallet::call]
177
    impl<T: Config> Pallet<T> {
178
        #[pallet::call_index(0)]
179
        #[pallet::weight(T::WeightInfo::set_inactivity_tracking_status())]
180
        pub fn set_inactivity_tracking_status(
181
            origin: OriginFor<T>,
182
            enable_inactivity_tracking: bool,
183
14
        ) -> DispatchResult {
184
14
            ensure_root(origin)?;
185
13
            let current_status_end_session_index = match <CurrentActivityTrackingStatus<T>>::get() {
186
9
                ActivityTrackingStatus::Enabled { start: _, end } => {
187
9
                    ensure!(
188
9
                        !enable_inactivity_tracking,
189
1
                        Error::<T>::ActivityTrackingStatusAlreadyEnabled
190
                    );
191
8
                    end
192
                }
193
4
                ActivityTrackingStatus::Disabled { end } => {
194
4
                    ensure!(
195
4
                        enable_inactivity_tracking,
196
1
                        Error::<T>::ActivityTrackingStatusAlreadyDisabled
197
                    );
198
3
                    end
199
                }
200
            };
201
11
            let current_session_index = T::CurrentSessionIndex::session_index();
202
11
            ensure!(
203
11
                current_session_index > current_status_end_session_index,
204
1
                Error::<T>::ActivityTrackingStatusUpdateSuspended
205
            );
206
10
            Self::set_inactivity_tracking_status_inner(
207
10
                current_session_index,
208
10
                enable_inactivity_tracking,
209
10
            );
210
10
            Ok(())
211
        }
212
    }
213

            
214
58598
    #[pallet::hooks]
215
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
216
29431
        fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
217
29431
            let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
218
            // Process the orchestrator chain block author (if it exists) and activity tracking is enabled
219
29431
            if let Some(orchestrator_chain_author) = T::GetSelfChainBlockAuthor::get_block_author()
220
            {
221
25621
                total_weight.saturating_accrue(T::DbWeight::get().reads(1));
222
25621
                if let ActivityTrackingStatus::Enabled { start, end: _ } =
223
25621
                    <CurrentActivityTrackingStatus<T>>::get()
224
                {
225
25621
                    total_weight.saturating_accrue(T::DbWeight::get().reads(1));
226
25621
                    if start <= T::CurrentSessionIndex::session_index() {
227
25621
                        total_weight
228
25621
                            .saturating_accrue(Self::on_author_noted(orchestrator_chain_author));
229
25621
                    }
230
                }
231
3810
            }
232
29431
            total_weight
233
29431
        }
234
    }
235

            
236
    impl<T: Config> Pallet<T> {
237
        /// Internal function to set the activity tracking status and
238
        /// clear ActiveCollatorsForCurrentSession if disabled
239
11
        fn set_inactivity_tracking_status_inner(
240
11
            current_session_index: SessionIndex,
241
11
            enable_inactivity_tracking: bool,
242
11
        ) {
243
11
            let new_status_end_session_index =
244
11
                current_session_index.saturating_add(T::MaxInactiveSessions::get());
245
11
            let new_status = if enable_inactivity_tracking {
246
3
                ActivityTrackingStatus::Enabled {
247
3
                    start: current_session_index.saturating_add(1),
248
3
                    end: new_status_end_session_index,
249
3
                }
250
            } else {
251
8
                <ActiveCollatorsForCurrentSession<T>>::put(BoundedBTreeSet::new());
252
8
                ActivityTrackingStatus::Disabled {
253
8
                    end: new_status_end_session_index,
254
8
                }
255
            };
256
11
            <CurrentActivityTrackingStatus<T>>::put(new_status.clone());
257
11
            Self::deposit_event(Event::<T>::ActivityTrackingStatusSet { status: new_status })
258
11
        }
259

            
260
        /// Internal function to clear the active collators for the current session
261
        /// and remove the collators records that are outside the activity period.
262
        /// Triggered at the beginning of each session.
263
3940
        pub fn process_ended_session() {
264
3940
            let current_session_index = T::CurrentSessionIndex::session_index();
265
3940
            <ActiveCollatorsForCurrentSession<T>>::put(BoundedBTreeSet::new());
266
3940

            
267
3940
            // Cleanup active collator info for sessions that are older than the maximum allowed
268
3940
            if current_session_index > T::MaxInactiveSessions::get() {
269
1523
                <crate::pallet::InactiveCollators<T>>::remove(
270
1523
                    current_session_index
271
1523
                        .saturating_sub(T::MaxInactiveSessions::get())
272
1523
                        .saturating_sub(1),
273
1523
                );
274
2987
            }
275
3940
        }
276

            
277
        /// Internal function to populate the inactivity tracking storage used for marking collator
278
        /// as inactive. Triggered at the end of a session.
279
2978
        pub fn on_before_session_ending() {
280
2978
            let current_session_index = T::CurrentSessionIndex::session_index();
281
2978
            match <CurrentActivityTrackingStatus<T>>::get() {
282
9
                ActivityTrackingStatus::Disabled { .. } => return,
283
2969
                ActivityTrackingStatus::Enabled { start, end: _ } => {
284
2969
                    if start > current_session_index {
285
3
                        return;
286
2966
                    }
287
                }
288
            }
289
2966
            if let Ok(inactive_collators) =
290
2966
                BoundedBTreeSet::<T::CollatorId, T::MaxCollatorsPerSession>::try_from(
291
2966
                    T::CurrentCollatorsFetcher::get_all_collators_assigned_to_chains(
292
2966
                        ForSession::Current,
293
2966
                    )
294
2966
                    .difference(&<ActiveCollatorsForCurrentSession<T>>::get())
295
2966
                    .cloned()
296
2966
                    .collect::<BTreeSet<T::CollatorId>>(),
297
2966
                )
298
2966
            {
299
2966
                InactiveCollators::<T>::insert(current_session_index, inactive_collators);
300
2966
            } else {
301
                // If we reach MaxCollatorsPerSession limit there must be a bug in the pallet
302
                // so we disable the activity tracking
303
                Self::set_inactivity_tracking_status_inner(current_session_index, false);
304
            }
305
2978
        }
306

            
307
        /// Internal update the current session active collator records.
308
        /// This function is called when a container chain or orchestrator chain collator is noted.
309
48101
        pub fn on_author_noted(author: T::CollatorId) -> Weight {
310
48101
            let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
311
48101
            let _ = <ActiveCollatorsForCurrentSession<T>>::try_mutate(
312
48101
                |active_collators| -> DispatchResult {
313
48101
                    if let Err(_) = active_collators.try_insert(author.clone()) {
314
                        // If we reach MaxCollatorsPerSession limit there must be a bug in the pallet
315
                        // so we disable the activity tracking
316
1
                        Self::set_inactivity_tracking_status_inner(
317
1
                            T::CurrentSessionIndex::session_index(),
318
1
                            false,
319
1
                        );
320
1
                        total_weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 2));
321
1
                        return Err(Error::<T>::MaxCollatorsPerSessionReached.into());
322
48100
                    } else {
323
48100
                        total_weight.saturating_accrue(T::DbWeight::get().writes(1));
324
48100
                    }
325
48100
                    Ok(())
326
48101
                },
327
48101
            );
328
48101
            total_weight
329
48101
        }
330
    }
331
}
332

            
333
impl<T: Config> NodeActivityTrackingHelper<T::CollatorId> for Pallet<T> {
334
49
    fn is_node_inactive(node: &T::CollatorId) -> bool {
335
49
        let current_session_index = T::CurrentSessionIndex::session_index();
336
49
        let minimum_sessions_required = T::MaxInactiveSessions::get();
337
49
        match <CurrentActivityTrackingStatus<T>>::get() {
338
2
            ActivityTrackingStatus::Disabled { .. } => return false,
339
47
            ActivityTrackingStatus::Enabled { start, end: _ } => {
340
47
                if start.saturating_add(minimum_sessions_required) > current_session_index {
341
20
                    return false;
342
27
                }
343
27
            }
344
27
        }
345
27

            
346
27
        let start_session_index = current_session_index.saturating_sub(minimum_sessions_required);
347
87
        for session_index in start_session_index..current_session_index {
348
87
            if !<InactiveCollators<T>>::get(session_index).contains(node) {
349
10
                return false;
350
77
            }
351
        }
352
17
        true
353
49
    }
354
}
355

            
356
impl<T: Config> AuthorNotingHook<T::CollatorId> for Pallet<T> {
357
22350
    fn on_container_authors_noted(info: &[AuthorNotingInfo<T::CollatorId>]) -> Weight {
358
22350
        let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
359
22349
        if let ActivityTrackingStatus::Enabled { start, end: _ } =
360
22350
            <CurrentActivityTrackingStatus<T>>::get()
361
        {
362
22349
            if start <= T::CurrentSessionIndex::session_index() {
363
44828
                for author_info in info {
364
22480
                    total_weight
365
22480
                        .saturating_accrue(Self::on_author_noted(author_info.author.clone()));
366
22480
                }
367
1
            }
368
1
        }
369
22350
        total_weight
370
22350
    }
371
    #[cfg(feature = "runtime-benchmarks")]
372
    fn prepare_worst_case_for_bench(_a: &T::CollatorId, _b: BlockNumber, _para_id: ParaId) {}
373
}