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
//! Invulnerables pallet.
18
//!
19
//! A pallet to manage invulnerable collators in a parachain.
20
//!
21
//! ## Terminology
22
//!
23
//! - Collator: A parachain block producer.
24
//! - Invulnerable: An account appointed by governance and guaranteed to be in the collator set.
25

            
26
#![cfg_attr(not(feature = "std"), no_std)]
27

            
28
pub use pallet::*;
29
use {
30
    core::marker::PhantomData,
31
    sp_runtime::{traits::Convert, TokenError},
32
};
33

            
34
#[cfg(test)]
35
mod mock;
36

            
37
#[cfg(test)]
38
mod tests;
39

            
40
#[cfg(feature = "runtime-benchmarks")]
41
mod benchmarking;
42
pub mod weights;
43

            
44
1
#[frame_support::pallet]
45
pub mod pallet {
46
    pub use crate::weights::WeightInfo;
47

            
48
    #[cfg(feature = "runtime-benchmarks")]
49
    use frame_support::traits::Currency;
50

            
51
    use {
52
        frame_support::{
53
            dispatch::DispatchResultWithPostInfo,
54
            pallet_prelude::*,
55
            traits::{EnsureOrigin, ValidatorRegistration},
56
            BoundedVec, DefaultNoBound,
57
        },
58
        frame_system::pallet_prelude::*,
59
        pallet_session::SessionManager,
60
        sp_runtime::traits::Convert,
61
        sp_staking::SessionIndex,
62
        sp_std::vec::Vec,
63
    };
64

            
65
    /// The current storage version.
66
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
67

            
68
    /// A convertor from collators id. Since this pallet does not have stash/controller, this is
69
    /// just identity.
70
    pub struct IdentityCollator;
71
    impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {
72
16728
        fn convert(t: T) -> Option<T> {
73
16728
            Some(t)
74
16728
        }
75
    }
76

            
77
    /// Configure the pallet by specifying the parameters and types on which it depends.
78
    #[pallet::config]
79
    pub trait Config: frame_system::Config {
80
        /// Overarching event type.
81
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
82

            
83
        /// Origin that can dictate updating parameters of this pallet.
84
        type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
85

            
86
        /// Maximum number of invulnerables.
87
        #[pallet::constant]
88
        type MaxInvulnerables: Get<u32>;
89

            
90
        /// A stable ID for a collator.
91
        type CollatorId: Member + Parameter + MaybeSerializeDeserialize + MaxEncodedLen + Ord;
92

            
93
        /// A conversion from account ID to collator ID.
94
        ///
95
        /// Its cost must be at most one storage read.
96
        type CollatorIdOf: Convert<Self::AccountId, Option<Self::CollatorId>>;
97

            
98
        /// Validate a user is registered
99
        type CollatorRegistration: ValidatorRegistration<Self::CollatorId>;
100

            
101
        /// The weight information of this pallet.
102
        type WeightInfo: WeightInfo;
103

            
104
        #[cfg(feature = "runtime-benchmarks")]
105
        type Currency: Currency<Self::AccountId>
106
            + frame_support::traits::fungible::Balanced<Self::AccountId>;
107
    }
108

            
109
13627
    #[pallet::pallet]
110
    #[pallet::storage_version(STORAGE_VERSION)]
111
    pub struct Pallet<T>(_);
112

            
113
    /// The invulnerable, permissioned collators. This list must be sorted.
114
43920
    #[pallet::storage]
115
    pub type Invulnerables<T: Config> =
116
        StorageValue<_, BoundedVec<T::CollatorId, T::MaxInvulnerables>, ValueQuery>;
117

            
118
    #[pallet::genesis_config]
119
    #[derive(DefaultNoBound)]
120
    pub struct GenesisConfig<T: Config> {
121
        pub invulnerables: Vec<T::CollatorId>,
122
    }
123

            
124
498
    #[pallet::genesis_build]
125
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
126
253
        fn build(&self) {
127
253
            let duplicate_invulnerables = self
128
253
                .invulnerables
129
253
                .iter()
130
253
                .collect::<sp_std::collections::btree_set::BTreeSet<_>>();
131
253
            assert!(
132
253
                duplicate_invulnerables.len() == self.invulnerables.len(),
133
                "duplicate invulnerables in genesis."
134
            );
135

            
136
253
            let bounded_invulnerables =
137
253
                BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())
138
253
                    .expect("genesis invulnerables are more than T::MaxInvulnerables");
139
253

            
140
253
            <Invulnerables<T>>::put(bounded_invulnerables);
141
253
        }
142
    }
143

            
144
    #[pallet::event]
145
81
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
146
    pub enum Event<T: Config> {
147
        /// New Invulnerables were set.
148
        NewInvulnerables { invulnerables: Vec<T::CollatorId> },
149
3
        /// A new Invulnerable was added.
150
        InvulnerableAdded { account_id: T::AccountId },
151
1
        /// An Invulnerable was removed.
152
        InvulnerableRemoved { account_id: T::AccountId },
153
        /// An account was unable to be added to the Invulnerables because they did not have keys
154
        /// registered. Other Invulnerables may have been set.
155
        InvalidInvulnerableSkipped { account_id: T::AccountId },
156
    }
157

            
158
10
    #[pallet::error]
159
    pub enum Error<T> {
160
        /// There are too many Invulnerables.
161
        TooManyInvulnerables,
162
        /// Account is already an Invulnerable.
163
        AlreadyInvulnerable,
164
        /// Account is not an Invulnerable.
165
        NotInvulnerable,
166
        /// Account does not have keys registered
167
        NoKeysRegistered,
168
        /// Unable to derive collator id from account id
169
        UnableToDeriveCollatorId,
170
    }
171

            
172
130
    #[pallet::call]
173
    impl<T: Config> Pallet<T> {
174
        /// Add a new account `who` to the list of `Invulnerables` collators.
175
        ///
176
        /// The origin for this call must be the `UpdateOrigin`.
177
        #[pallet::call_index(1)]
178
        #[pallet::weight(T::WeightInfo::add_invulnerable(
179
			T::MaxInvulnerables::get().saturating_sub(1),
180
		))]
181
        pub fn add_invulnerable(
182
            origin: OriginFor<T>,
183
            who: T::AccountId,
184
54
        ) -> DispatchResultWithPostInfo {
185
54
            T::UpdateOrigin::ensure_origin(origin)?;
186
            // don't let one unprepared collator ruin things for everyone.
187
53
            let maybe_collator_id = T::CollatorIdOf::convert(who.clone())
188
53
                .filter(T::CollatorRegistration::is_registered);
189

            
190
53
            let collator_id = maybe_collator_id.ok_or(Error::<T>::NoKeysRegistered)?;
191

            
192
52
            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
193
52
                if invulnerables.contains(&collator_id) {
194
3
                    Err(Error::<T>::AlreadyInvulnerable)?;
195
49
                }
196
49
                invulnerables
197
49
                    .try_push(collator_id.clone())
198
49
                    .map_err(|_| Error::<T>::TooManyInvulnerables)?;
199
48
                Ok(())
200
52
            })?;
201

            
202
48
            Self::deposit_event(Event::InvulnerableAdded { account_id: who });
203
48

            
204
48
            let weight_used = T::WeightInfo::add_invulnerable(
205
48
                Invulnerables::<T>::decode_len()
206
48
                    .unwrap_or_default()
207
48
                    .try_into()
208
48
                    .unwrap_or(T::MaxInvulnerables::get().saturating_sub(1)),
209
48
            );
210
48

            
211
48
            Ok(Some(weight_used).into())
212
        }
213

            
214
        /// Remove an account `who` from the list of `Invulnerables` collators. `Invulnerables` must
215
        /// be sorted.
216
        ///
217
        /// The origin for this call must be the `UpdateOrigin`.
218
        #[pallet::call_index(2)]
219
        #[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxInvulnerables::get()))]
220
35
        pub fn remove_invulnerable(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
221
35
            T::UpdateOrigin::ensure_origin(origin)?;
222

            
223
34
            let collator_id = T::CollatorIdOf::convert(who.clone())
224
34
                .ok_or(Error::<T>::UnableToDeriveCollatorId)?;
225

            
226
34
            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
227
34
                let pos = invulnerables
228
34
                    .iter()
229
39
                    .position(|x| x == &collator_id)
230
34
                    .ok_or(Error::<T>::NotInvulnerable)?;
231
33
                invulnerables.remove(pos);
232
33
                Ok(())
233
34
            })?;
234

            
235
33
            Self::deposit_event(Event::InvulnerableRemoved { account_id: who });
236
33
            Ok(())
237
        }
238
    }
239

            
240
    impl<T: Config> Pallet<T> {
241
2470
        pub fn invulnerables() -> BoundedVec<T::CollatorId, T::MaxInvulnerables> {
242
2470
            Invulnerables::<T>::get()
243
2470
        }
244
    }
245

            
246
    /// Play the role of the session manager.
247
    impl<T: Config> SessionManager<T::CollatorId> for Pallet<T> {
248
12
        fn new_session(index: SessionIndex) -> Option<Vec<T::CollatorId>> {
249
12
            log::info!(
250
                "assembling new invulnerable collators for new session {} at #{:?}",
251
                index,
252
                <frame_system::Pallet<T>>::block_number(),
253
            );
254

            
255
12
            let invulnerables = Self::invulnerables().to_vec();
256
12
            frame_system::Pallet::<T>::register_extra_weight_unchecked(
257
12
                T::WeightInfo::new_session(invulnerables.len() as u32),
258
12
                DispatchClass::Mandatory,
259
12
            );
260
12
            Some(invulnerables)
261
12
        }
262
6
        fn start_session(_: SessionIndex) {
263
6
            // we don't care.
264
6
        }
265
        fn end_session(_: SessionIndex) {
266
            // we don't care.
267
        }
268
    }
269
}
270

            
271
/// If the rewarded account is an Invulnerable, distribute the entire reward
272
/// amount to them. Otherwise use the `Fallback` distribution.
273
pub struct InvulnerableRewardDistribution<Runtime, Currency, Fallback>(
274
    PhantomData<(Runtime, Currency, Fallback)>,
275
);
276

            
277
use {frame_support::pallet_prelude::Weight, sp_runtime::traits::Get};
278

            
279
type CreditOf<Runtime, Currency> =
280
    frame_support::traits::fungible::Credit<<Runtime as frame_system::Config>::AccountId, Currency>;
281
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
282

            
283
impl<Runtime, Currency, Fallback>
284
    tp_traits::DistributeRewards<AccountIdOf<Runtime>, CreditOf<Runtime, Currency>>
285
    for InvulnerableRewardDistribution<Runtime, Currency, Fallback>
286
where
287
    Runtime: frame_system::Config + Config,
288
    Fallback: tp_traits::DistributeRewards<AccountIdOf<Runtime>, CreditOf<Runtime, Currency>>,
289
    Currency: frame_support::traits::fungible::Balanced<AccountIdOf<Runtime>>,
290
{
291
16580
    fn distribute_rewards(
292
16580
        rewarded: AccountIdOf<Runtime>,
293
16580
        amount: CreditOf<Runtime, Currency>,
294
16580
    ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
295
16580
        let mut total_weight = Weight::zero();
296
16580
        let collator_id = Runtime::CollatorIdOf::convert(rewarded.clone())
297
16580
            .ok_or(Error::<Runtime>::UnableToDeriveCollatorId)?;
298
        // weight to read invulnerables
299
16580
        total_weight += Runtime::DbWeight::get().reads(1);
300
16580
        if !Invulnerables::<Runtime>::get().contains(&collator_id) {
301
397
            let post_info = Fallback::distribute_rewards(rewarded, amount)?;
302
364
            if let Some(weight) = post_info.actual_weight {
303
338
                total_weight += weight;
304
364
            }
305
        } else {
306
16183
            Currency::resolve(&rewarded, amount).map_err(|_| TokenError::NotExpendable)?;
307
16183
            total_weight +=
308
16183
                Runtime::WeightInfo::reward_invulnerable(Runtime::MaxInvulnerables::get())
309
        }
310
16547
        Ok(Some(total_weight).into())
311
16580
    }
312
}