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
8104
#[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
            pallet_prelude::*,
54
            traits::{EnsureOrigin, ValidatorRegistration},
55
            BoundedVec, DefaultNoBound,
56
        },
57
        frame_system::pallet_prelude::*,
58
        pallet_session::SessionManager,
59
        sp_runtime::traits::Convert,
60
        sp_staking::SessionIndex,
61
        sp_std::vec::Vec,
62
    };
63

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

            
67
    /// Configure the pallet by specifying the parameters and types on which it depends.
68
    #[pallet::config]
69
    pub trait Config: frame_system::Config {
70
        /// Overarching event type.
71
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
72

            
73
        /// Origin that can dictate updating parameters of this pallet.
74
        type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
75

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

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

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

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

            
91
        /// The weight information of this pallet.
92
        type WeightInfo: WeightInfo;
93

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

            
99
46880
    #[pallet::pallet]
100
    #[pallet::storage_version(STORAGE_VERSION)]
101
    pub struct Pallet<T>(_);
102

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

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

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

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

            
130
1125
            <Invulnerables<T>>::put(bounded_invulnerables);
131
1125
        }
132
    }
133

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

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

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

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

            
174
119
            <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
175
119
                if invulnerables.contains(&collator_id) {
176
13
                    Err(Error::<T>::AlreadyInvulnerable)?;
177
106
                }
178
106
                invulnerables
179
106
                    .try_push(collator_id.clone())
180
106
                    .map_err(|_| Error::<T>::TooManyInvulnerables)?;
181
105
                Ok(())
182
119
            })?;
183

            
184
105
            Self::deposit_event(Event::InvulnerableAdded { account_id: who });
185
105

            
186
105
            Ok(())
187
        }
188

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

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

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

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

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

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

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

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

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

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

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