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
extern crate alloc;
28

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

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

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

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

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

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

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

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

            
68
    /// Configure the pallet by specifying the parameters and types on which it depends.
69
    #[pallet::config]
70
    pub trait Config: frame_system::Config {
71
        /// Origin that can dictate updating parameters of this pallet.
72
        type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
73

            
74
        /// Maximum number of invulnerables.
75
        #[pallet::constant]
76
        type MaxInvulnerables: Get<u32>;
77

            
78
        /// A stable ID for a collator.
79
        type CollatorId: Member + Parameter + MaybeSerializeDeserialize + MaxEncodedLen + Ord;
80

            
81
        /// A conversion from account ID to collator ID.
82
        ///
83
        /// Its cost must be at most one storage read.
84
        type CollatorIdOf: Convert<Self::AccountId, Option<Self::CollatorId>>;
85

            
86
        /// Validate a user is registered
87
        type CollatorRegistration: ValidatorRegistration<Self::CollatorId>;
88

            
89
        /// The weight information of this pallet.
90
        type WeightInfo: WeightInfo;
91

            
92
        #[cfg(feature = "runtime-benchmarks")]
93
        type Currency: Currency<Self::AccountId>
94
            + frame_support::traits::fungible::Balanced<Self::AccountId>;
95
    }
96

            
97
    #[pallet::pallet]
98
    #[pallet::storage_version(STORAGE_VERSION)]
99
    pub struct Pallet<T>(_);
100

            
101
    /// The invulnerable, permissioned collators.
102
    #[pallet::storage]
103
    pub type Invulnerables<T: Config> =
104
        StorageValue<_, BoundedVec<T::CollatorId, T::MaxInvulnerables>, ValueQuery>;
105

            
106
    #[pallet::genesis_config]
107
    #[derive(DefaultNoBound)]
108
    pub struct GenesisConfig<T: Config> {
109
        pub invulnerables: Vec<T::CollatorId>,
110
    }
111

            
112
    #[pallet::genesis_build]
113
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
114
1683
        fn build(&self) {
115
1683
            let duplicate_invulnerables = self
116
1683
                .invulnerables
117
1683
                .iter()
118
1683
                .collect::<alloc::collections::btree_set::BTreeSet<_>>();
119
1683
            assert!(
120
1683
                duplicate_invulnerables.len() == self.invulnerables.len(),
121
                "duplicate invulnerables in genesis."
122
            );
123

            
124
1683
            let bounded_invulnerables =
125
1683
                BoundedVec::<_, T::MaxInvulnerables>::try_from(self.invulnerables.clone())
126
1683
                    .expect("genesis invulnerables are more than T::MaxInvulnerables");
127

            
128
1683
            <Invulnerables<T>>::put(bounded_invulnerables);
129
1683
        }
130
    }
131

            
132
    #[pallet::event]
133
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
134
    pub enum Event<T: Config> {
135
        /// A new Invulnerable was added.
136
        InvulnerableAdded { account_id: T::AccountId },
137
        /// An Invulnerable was removed.
138
        InvulnerableRemoved { account_id: T::AccountId },
139
    }
140

            
141
    #[pallet::error]
142
    pub enum Error<T> {
143
        /// There are too many Invulnerables.
144
        TooManyInvulnerables,
145
        /// Account is already an Invulnerable.
146
        AlreadyInvulnerable,
147
        /// Account is not an Invulnerable.
148
        NotInvulnerable,
149
        /// Account does not have keys registered
150
        NoKeysRegistered,
151
        /// Unable to derive collator id from account id
152
        UnableToDeriveCollatorId,
153
    }
154

            
155
    #[pallet::call]
156
    impl<T: Config> Pallet<T> {
157
        /// Add a new account `who` to the list of `Invulnerables` collators.
158
        ///
159
        /// The origin for this call must be the `UpdateOrigin`.
160
        #[pallet::call_index(1)]
161
        #[pallet::weight(T::WeightInfo::add_invulnerable(
162
			T::MaxInvulnerables::get().saturating_sub(1),
163
		))]
164
        #[allow(clippy::useless_conversion)]
165
55
        pub fn add_invulnerable(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
166
55
            T::UpdateOrigin::ensure_origin(origin)?;
167
            // don't let one unprepared collator ruin things for everyone.
168
54
            let maybe_collator_id = T::CollatorIdOf::convert(who.clone())
169
54
                .filter(T::CollatorRegistration::is_registered);
170

            
171
54
            let collator_id = maybe_collator_id.ok_or(Error::<T>::NoKeysRegistered)?;
172

            
173
53
            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
174
53
                if invulnerables.contains(&collator_id) {
175
1
                    Err(Error::<T>::AlreadyInvulnerable)?;
176
52
                }
177
52
                invulnerables
178
52
                    .try_push(collator_id.clone())
179
52
                    .map_err(|_| Error::<T>::TooManyInvulnerables)?;
180
51
                Ok(())
181
53
            })?;
182

            
183
51
            Self::deposit_event(Event::InvulnerableAdded { account_id: who });
184

            
185
51
            Ok(())
186
        }
187

            
188
        /// Remove an account `who` from the list of `Invulnerables` collators.
189
        ///
190
        /// The origin for this call must be the `UpdateOrigin`.
191
        #[pallet::call_index(2)]
192
        #[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxInvulnerables::get()))]
193
23
        pub fn remove_invulnerable(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
194
23
            T::UpdateOrigin::ensure_origin(origin)?;
195

            
196
22
            let collator_id = T::CollatorIdOf::convert(who.clone())
197
22
                .ok_or(Error::<T>::UnableToDeriveCollatorId)?;
198

            
199
22
            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
200
22
                let pos = invulnerables
201
22
                    .iter()
202
37
                    .position(|x| x == &collator_id)
203
22
                    .ok_or(Error::<T>::NotInvulnerable)?;
204
21
                invulnerables.remove(pos);
205
21
                Ok(())
206
22
            })?;
207

            
208
21
            Self::deposit_event(Event::InvulnerableRemoved { account_id: who });
209
21
            Ok(())
210
        }
211
    }
212

            
213
    impl<T: Config> Pallet<T> {
214
8155
        pub fn invulnerables() -> BoundedVec<T::CollatorId, T::MaxInvulnerables> {
215
8155
            Invulnerables::<T>::get()
216
8155
        }
217
    }
218

            
219
    /// Play the role of the session manager.
220
    impl<T: Config> SessionManager<T::CollatorId> for Pallet<T> {
221
12
        fn new_session(index: SessionIndex) -> Option<Vec<T::CollatorId>> {
222
12
            log::info!(
223
                "assembling new invulnerable collators for new session {} at #{:?}",
224
                index,
225
                <frame_system::Pallet<T>>::block_number(),
226
            );
227

            
228
12
            let invulnerables = Self::invulnerables().to_vec();
229
12
            frame_system::Pallet::<T>::register_extra_weight_unchecked(
230
12
                T::WeightInfo::new_session(invulnerables.len() as u32),
231
12
                DispatchClass::Mandatory,
232
            );
233
12
            Some(invulnerables)
234
12
        }
235
6
        fn start_session(_: SessionIndex) {
236
            // we don't care.
237
6
        }
238
        fn end_session(_: SessionIndex) {
239
            // we don't care.
240
        }
241
    }
242
}
243

            
244
/// If the rewarded account is an Invulnerable, distribute the entire reward
245
/// amount to them. Otherwise use the `Fallback` distribution.
246
pub struct InvulnerableRewardDistribution<Runtime, Currency, Fallback>(
247
    PhantomData<(Runtime, Currency, Fallback)>,
248
);
249

            
250
use {frame_support::pallet_prelude::Weight, sp_runtime::traits::Get};
251

            
252
type CreditOf<Runtime, Currency> =
253
    frame_support::traits::fungible::Credit<<Runtime as frame_system::Config>::AccountId, Currency>;
254
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
255

            
256
impl<Runtime, Currency, Fallback>
257
    tp_traits::DistributeRewards<AccountIdOf<Runtime>, CreditOf<Runtime, Currency>>
258
    for InvulnerableRewardDistribution<Runtime, Currency, Fallback>
259
where
260
    Runtime: frame_system::Config + Config,
261
    Fallback: tp_traits::DistributeRewards<AccountIdOf<Runtime>, CreditOf<Runtime, Currency>>,
262
    Currency: frame_support::traits::fungible::Balanced<AccountIdOf<Runtime>>,
263
{
264
6374
    fn distribute_rewards(
265
6374
        rewarded: AccountIdOf<Runtime>,
266
6374
        amount: CreditOf<Runtime, Currency>,
267
6374
    ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
268
6374
        let mut total_weight = Weight::zero();
269
6374
        let collator_id = Runtime::CollatorIdOf::convert(rewarded.clone())
270
6374
            .ok_or(Error::<Runtime>::UnableToDeriveCollatorId)?;
271
        // weight to read invulnerables
272
6374
        total_weight.saturating_accrue(Runtime::DbWeight::get().reads(1));
273
6374
        if !Invulnerables::<Runtime>::get().contains(&collator_id) {
274
104
            let post_info = Fallback::distribute_rewards(rewarded, amount)?;
275
38
            if let Some(weight) = post_info.actual_weight {
276
12
                total_weight.saturating_accrue(weight);
277
38
            }
278
        } else {
279
6270
            Currency::resolve(&rewarded, amount).map_err(|_| TokenError::NotExpendable)?;
280
6270
            total_weight.saturating_accrue(Runtime::WeightInfo::reward_invulnerable(
281
6270
                Runtime::MaxInvulnerables::get(),
282
            ))
283
        }
284
6308
        Ok(Some(total_weight).into())
285
6374
    }
286
}