1
// Copyright 2019-2022 PureStake Inc.
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam 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
// Moonbeam 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 Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! # Pallet moonbeam-orbiters
18
//!
19
//! This pallet allows authorized collators to share their block creation rights and rewards with
20
//! multiple entities named "orbiters".
21
//! Each authorized collator will define a group of orbiters, and each orbiter will replace the
22
//! collator in turn with the other orbiters (rotation every `RotatePeriod` rounds).
23
//!
24
//! This pallet is designed to work with the nimbus consensus.
25
//! In order not to impact the other pallets (notably nimbus and parachain-staking) this pallet
26
//! simply redefines the lookup NimbusId-> AccountId, in order to replace the collator by its
27
//! currently selected orbiter.
28

            
29
#![cfg_attr(not(feature = "std"), no_std)]
30

            
31
pub mod types;
32
pub mod weights;
33

            
34
#[cfg(any(test, feature = "runtime-benchmarks"))]
35
mod benchmarks;
36
#[cfg(test)]
37
mod mock;
38
#[cfg(test)]
39
mod tests;
40

            
41
pub use pallet::*;
42
pub use types::*;
43
pub use weights::WeightInfo;
44

            
45
use frame_support::pallet;
46
use nimbus_primitives::{AccountLookup, NimbusId};
47

            
48
102
#[pallet]
49
pub mod pallet {
50
	use super::*;
51
	use frame_support::pallet_prelude::*;
52
	use frame_support::traits::{Currency, NamedReservableCurrency};
53
	use frame_system::pallet_prelude::*;
54
	use sp_runtime::traits::{CheckedSub, One, Saturating, StaticLookup, Zero};
55

            
56
30
	#[pallet::pallet]
57
	#[pallet::without_storage_info]
58
	pub struct Pallet<T>(PhantomData<T>);
59

            
60
	pub type BalanceOf<T> =
61
		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
62

            
63
	pub type ReserveIdentifierOf<T> = <<T as Config>::Currency as NamedReservableCurrency<
64
		<T as frame_system::Config>::AccountId,
65
	>>::ReserveIdentifier;
66

            
67
	#[pallet::config]
68
	pub trait Config: frame_system::Config {
69
		/// Overarching event type.
70
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
71

            
72
		/// A type to convert between AuthorId and AccountId. This pallet wrap the lookup to allow
73
		/// orbiters authoring.
74
		type AccountLookup: AccountLookup<Self::AccountId>;
75

            
76
		/// Origin that is allowed to add a collator in orbiters program.
77
		type AddCollatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
78

            
79
		/// The currency type.
80
		type Currency: NamedReservableCurrency<Self::AccountId>;
81

            
82
		/// Origin that is allowed to remove a collator from orbiters program.
83
		type DelCollatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
84

            
85
		#[pallet::constant]
86
		/// Maximum number of orbiters per collator.
87
		type MaxPoolSize: Get<u32>;
88

            
89
		#[pallet::constant]
90
		/// Maximum number of round to keep on storage.
91
		type MaxRoundArchive: Get<Self::RoundIndex>;
92

            
93
		/// Reserve identifier for this pallet instance.
94
		type OrbiterReserveIdentifier: Get<ReserveIdentifierOf<Self>>;
95

            
96
		#[pallet::constant]
97
		/// Number of rounds before changing the selected orbiter.
98
		/// WARNING: when changing `RotatePeriod`, you need a migration code that sets
99
		/// `ForceRotation` to true to avoid holes in `OrbiterPerRound`.
100
		type RotatePeriod: Get<Self::RoundIndex>;
101

            
102
		/// Round index type.
103
		type RoundIndex: Parameter
104
			+ Member
105
			+ MaybeSerializeDeserialize
106
			+ sp_std::fmt::Debug
107
			+ Default
108
			+ sp_runtime::traits::MaybeDisplay
109
			+ sp_runtime::traits::AtLeast32Bit
110
			+ Copy;
111

            
112
		/// Weight information for extrinsics in this pallet.
113
		type WeightInfo: WeightInfo;
114
	}
115

            
116
58904
	#[pallet::storage]
117
	#[pallet::getter(fn account_lookup_override)]
118
	/// Account lookup override
119
	pub type AccountLookupOverride<T: Config> =
120
		StorageMap<_, Blake2_128Concat, T::AccountId, Option<T::AccountId>>;
121

            
122
206
	#[pallet::storage]
123
	#[pallet::getter(fn collators_pool)]
124
	/// Current orbiters, with their "parent" collator
125
	pub type CollatorsPool<T: Config> =
126
		CountedStorageMap<_, Blake2_128Concat, T::AccountId, CollatorPoolInfo<T::AccountId>>;
127

            
128
374
	#[pallet::storage]
129
	/// Current round index
130
	pub(crate) type CurrentRound<T: Config> = StorageValue<_, T::RoundIndex, ValueQuery>;
131

            
132
220
	#[pallet::storage]
133
	/// If true, it forces the rotation at the next round.
134
	/// A use case: when changing RotatePeriod, you need a migration code that sets this value to
135
	/// true to avoid holes in OrbiterPerRound.
136
	pub(crate) type ForceRotation<T: Config> = StorageValue<_, bool, ValueQuery>;
137

            
138
42
	#[pallet::storage]
139
	#[pallet::getter(fn min_orbiter_deposit)]
140
	/// Minimum deposit required to be registered as an orbiter
141
	pub type MinOrbiterDeposit<T: Config> = StorageValue<_, BalanceOf<T>, OptionQuery>;
142

            
143
58
	#[pallet::storage]
144
	/// Store active orbiter per round and per parent collator
145
	pub(crate) type OrbiterPerRound<T: Config> = StorageDoubleMap<
146
		_,
147
		Twox64Concat,
148
		T::RoundIndex,
149
		Blake2_128Concat,
150
		T::AccountId,
151
		T::AccountId,
152
		OptionQuery,
153
	>;
154

            
155
19
	#[pallet::storage]
156
	#[pallet::getter(fn orbiter)]
157
	/// Check if account is an orbiter
158
	pub type RegisteredOrbiter<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, bool>;
159

            
160
	#[pallet::genesis_config]
161
	pub struct GenesisConfig<T: Config> {
162
		pub min_orbiter_deposit: BalanceOf<T>,
163
	}
164

            
165
	impl<T: Config> Default for GenesisConfig<T> {
166
2
		fn default() -> Self {
167
2
			Self {
168
2
				min_orbiter_deposit: One::one(),
169
2
			}
170
2
		}
171
	}
172

            
173
8
	#[pallet::genesis_build]
174
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
175
10
		fn build(&self) {
176
10
			assert!(
177
10
				self.min_orbiter_deposit > Zero::zero(),
178
				"Minimal orbiter deposit should be greater than zero"
179
			);
180
10
			MinOrbiterDeposit::<T>::put(self.min_orbiter_deposit)
181
10
		}
182
	}
183

            
184
70
	#[pallet::hooks]
185
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
186
77
		fn on_initialize(_: BlockNumberFor<T>) -> Weight {
187
			// Prune old OrbiterPerRound entries
188
4
			if let Some(round_to_prune) =
189
77
				CurrentRound::<T>::get().checked_sub(&T::MaxRoundArchive::get())
190
			{
191
				// TODO: Find better limit.
192
				// Is it sure to be cleared in a single block? In which case we can probably have
193
				// a lower limit.
194
				// Otherwise, we should still have a lower limit, and implement a multi-block clear
195
				// by using the return value of clear_prefix for subsequent blocks.
196
4
				let result = OrbiterPerRound::<T>::clear_prefix(round_to_prune, u32::MAX, None);
197
4
				T::WeightInfo::on_initialize(result.unique)
198
			} else {
199
73
				T::DbWeight::get().reads(1)
200
			}
201
77
		}
202
	}
203

            
204
	/// An error that can occur while executing this pallet's extrinsics.
205
18
	#[pallet::error]
206
	pub enum Error<T> {
207
		/// The collator is already added in orbiters program.
208
		CollatorAlreadyAdded,
209
		/// This collator is not in orbiters program.
210
		CollatorNotFound,
211
		/// There are already too many orbiters associated with this collator.
212
		CollatorPoolTooLarge,
213
		/// There are more collator pools than the number specified in the parameter.
214
		CollatorsPoolCountTooLow,
215
		/// The minimum deposit required to register as an orbiter has not yet been included in the
216
		/// onchain storage
217
		MinOrbiterDepositNotSet,
218
		/// This orbiter is already associated with this collator.
219
		OrbiterAlreadyInPool,
220
		/// This orbiter has not made a deposit
221
		OrbiterDepositNotFound,
222
		/// This orbiter is not found
223
		OrbiterNotFound,
224
		/// The orbiter is still at least in one pool
225
		OrbiterStillInAPool,
226
	}
227

            
228
	#[pallet::event]
229
23
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
230
	pub enum Event<T: Config> {
231
7
		/// An orbiter join a collator pool
232
		OrbiterJoinCollatorPool {
233
			collator: T::AccountId,
234
			orbiter: T::AccountId,
235
		},
236
3
		/// An orbiter leave a collator pool
237
		OrbiterLeaveCollatorPool {
238
			collator: T::AccountId,
239
			orbiter: T::AccountId,
240
		},
241
		/// Paid the orbiter account the balance as liquid rewards.
242
		OrbiterRewarded {
243
			account: T::AccountId,
244
			rewards: BalanceOf<T>,
245
		},
246
3
		OrbiterRotation {
247
			collator: T::AccountId,
248
			old_orbiter: Option<T::AccountId>,
249
			new_orbiter: Option<T::AccountId>,
250
		},
251
14
		/// An orbiter has registered
252
		OrbiterRegistered {
253
			account: T::AccountId,
254
			deposit: BalanceOf<T>,
255
		},
256
2
		/// An orbiter has unregistered
257
		OrbiterUnregistered { account: T::AccountId },
258
	}
259

            
260
	#[pallet::call]
261
	impl<T: Config> Pallet<T> {
262
		/// Add an orbiter in a collator pool
263
		#[pallet::call_index(0)]
264
		#[pallet::weight(T::WeightInfo::collator_add_orbiter())]
265
		pub fn collator_add_orbiter(
266
			origin: OriginFor<T>,
267
			orbiter: <T::Lookup as StaticLookup>::Source,
268
11
		) -> DispatchResult {
269
11
			let collator = ensure_signed(origin)?;
270
11
			let orbiter = T::Lookup::lookup(orbiter)?;
271

            
272
10
			let mut collator_pool =
273
11
				CollatorsPool::<T>::get(&collator).ok_or(Error::<T>::CollatorNotFound)?;
274
10
			let orbiters = collator_pool.get_orbiters();
275
10
			ensure!(
276
10
				(orbiters.len() as u32) < T::MaxPoolSize::get(),
277
1
				Error::<T>::CollatorPoolTooLarge
278
			);
279
9
			if orbiters.iter().any(|orbiter_| orbiter_ == &orbiter) {
280
1
				return Err(Error::<T>::OrbiterAlreadyInPool.into());
281
8
			}
282
8

            
283
8
			// Make sure the orbiter has made a deposit. It can be an old orbiter whose deposit
284
8
			// is lower than the current minimum (if the minimum was lower in the past), so we just
285
8
			// have to check that a deposit exists (which means checking that the deposit amount
286
8
			// is not zero).
287
8
			let orbiter_deposit =
288
8
				T::Currency::reserved_balance_named(&T::OrbiterReserveIdentifier::get(), &orbiter);
289
8
			ensure!(
290
8
				orbiter_deposit > BalanceOf::<T>::zero(),
291
1
				Error::<T>::OrbiterDepositNotFound
292
			);
293

            
294
7
			collator_pool.add_orbiter(orbiter.clone());
295
7
			CollatorsPool::<T>::insert(&collator, collator_pool);
296
7

            
297
7
			Self::deposit_event(Event::OrbiterJoinCollatorPool { collator, orbiter });
298
7

            
299
7
			Ok(())
300
		}
301

            
302
		/// Remove an orbiter from the caller collator pool
303
		#[pallet::call_index(1)]
304
		#[pallet::weight(T::WeightInfo::collator_remove_orbiter())]
305
		pub fn collator_remove_orbiter(
306
			origin: OriginFor<T>,
307
			orbiter: <T::Lookup as StaticLookup>::Source,
308
5
		) -> DispatchResult {
309
5
			let collator = ensure_signed(origin)?;
310
5
			let orbiter = T::Lookup::lookup(orbiter)?;
311

            
312
5
			Self::do_remove_orbiter_from_pool(collator, orbiter)
313
		}
314

            
315
		/// Remove the caller from the specified collator pool
316
		#[pallet::call_index(2)]
317
		#[pallet::weight(T::WeightInfo::orbiter_leave_collator_pool())]
318
		pub fn orbiter_leave_collator_pool(
319
			origin: OriginFor<T>,
320
			collator: <T::Lookup as StaticLookup>::Source,
321
		) -> DispatchResult {
322
			let orbiter = ensure_signed(origin)?;
323
			let collator = T::Lookup::lookup(collator)?;
324

            
325
			Self::do_remove_orbiter_from_pool(collator, orbiter)
326
		}
327

            
328
		/// Registering as an orbiter
329
		#[pallet::call_index(3)]
330
		#[pallet::weight(T::WeightInfo::orbiter_register())]
331
11
		pub fn orbiter_register(origin: OriginFor<T>) -> DispatchResult {
332
11
			let orbiter = ensure_signed(origin)?;
333

            
334
11
			if let Some(min_orbiter_deposit) = MinOrbiterDeposit::<T>::get() {
335
				// The use of `ensure_reserved_named` allows to update the deposit amount in case a
336
				// deposit has already been made.
337
11
				T::Currency::ensure_reserved_named(
338
11
					&T::OrbiterReserveIdentifier::get(),
339
11
					&orbiter,
340
11
					min_orbiter_deposit,
341
11
				)?;
342
10
				RegisteredOrbiter::<T>::insert(&orbiter, true);
343
10
				Self::deposit_event(Event::OrbiterRegistered {
344
10
					account: orbiter,
345
10
					deposit: min_orbiter_deposit,
346
10
				});
347
10
				Ok(())
348
			} else {
349
				Err(Error::<T>::MinOrbiterDepositNotSet.into())
350
			}
351
		}
352

            
353
		/// Deregistering from orbiters
354
		#[pallet::call_index(4)]
355
		#[pallet::weight(T::WeightInfo::orbiter_unregister(*collators_pool_count))]
356
		pub fn orbiter_unregister(
357
			origin: OriginFor<T>,
358
			collators_pool_count: u32,
359
2
		) -> DispatchResult {
360
2
			let orbiter = ensure_signed(origin)?;
361

            
362
			// We have to make sure that the `collators_pool_count` parameter is large enough,
363
			// because its value is used to calculate the weight of this extrinsic
364
2
			ensure!(
365
2
				collators_pool_count >= CollatorsPool::<T>::count(),
366
1
				Error::<T>::CollatorsPoolCountTooLow
367
			);
368

            
369
			// Ensure that the orbiter is not in any pool
370
1
			ensure!(
371
1
				!CollatorsPool::<T>::iter_values()
372
1
					.any(|collator_pool| collator_pool.contains_orbiter(&orbiter)),
373
				Error::<T>::OrbiterStillInAPool,
374
			);
375

            
376
1
			T::Currency::unreserve_all_named(&T::OrbiterReserveIdentifier::get(), &orbiter);
377
1
			RegisteredOrbiter::<T>::remove(&orbiter);
378
1
			Self::deposit_event(Event::OrbiterUnregistered { account: orbiter });
379
1

            
380
1
			Ok(())
381
		}
382

            
383
		/// Add a collator to orbiters program.
384
		#[pallet::call_index(5)]
385
		#[pallet::weight(T::WeightInfo::add_collator())]
386
		pub fn add_collator(
387
			origin: OriginFor<T>,
388
			collator: <T::Lookup as StaticLookup>::Source,
389
7
		) -> DispatchResult {
390
7
			T::AddCollatorOrigin::ensure_origin(origin)?;
391
7
			let collator = T::Lookup::lookup(collator)?;
392

            
393
7
			ensure!(
394
7
				CollatorsPool::<T>::get(&collator).is_none(),
395
1
				Error::<T>::CollatorAlreadyAdded
396
			);
397

            
398
6
			CollatorsPool::<T>::insert(collator, CollatorPoolInfo::default());
399
6

            
400
6
			Ok(())
401
		}
402

            
403
		/// Remove a collator from orbiters program.
404
		#[pallet::call_index(6)]
405
		#[pallet::weight(T::WeightInfo::remove_collator())]
406
		pub fn remove_collator(
407
			origin: OriginFor<T>,
408
			collator: <T::Lookup as StaticLookup>::Source,
409
		) -> DispatchResult {
410
			T::DelCollatorOrigin::ensure_origin(origin)?;
411
			let collator = T::Lookup::lookup(collator)?;
412

            
413
			// Remove the pool associated to this collator
414
			let collator_pool =
415
				CollatorsPool::<T>::take(&collator).ok_or(Error::<T>::CollatorNotFound)?;
416

            
417
			// Remove all AccountLookupOverride entries related to this collator
418
			for orbiter in collator_pool.get_orbiters() {
419
				AccountLookupOverride::<T>::remove(&orbiter);
420
			}
421
			AccountLookupOverride::<T>::remove(&collator);
422

            
423
			Ok(())
424
		}
425
	}
426

            
427
	impl<T: Config> Pallet<T> {
428
5
		fn do_remove_orbiter_from_pool(
429
5
			collator: T::AccountId,
430
5
			orbiter: T::AccountId,
431
5
		) -> DispatchResult {
432
4
			let mut collator_pool =
433
5
				CollatorsPool::<T>::get(&collator).ok_or(Error::<T>::CollatorNotFound)?;
434

            
435
4
			match collator_pool.remove_orbiter(&orbiter) {
436
				RemoveOrbiterResult::OrbiterNotFound => {
437
2
					return Err(Error::<T>::OrbiterNotFound.into())
438
				}
439
2
				RemoveOrbiterResult::OrbiterRemoved => {
440
2
					Self::deposit_event(Event::OrbiterLeaveCollatorPool {
441
2
						collator: collator.clone(),
442
2
						orbiter,
443
2
					});
444
2
				}
445
				RemoveOrbiterResult::OrbiterRemoveScheduled => (),
446
			}
447

            
448
2
			CollatorsPool::<T>::insert(collator, collator_pool);
449
2
			Ok(())
450
5
		}
451
59
		fn on_rotate(round_index: T::RoundIndex) -> Weight {
452
59
			let mut writes = 1;
453
59
			// Update current orbiter for each pool and edit AccountLookupOverride accordingly.
454
59
			CollatorsPool::<T>::translate::<CollatorPoolInfo<T::AccountId>, _>(
455
59
				|collator, mut pool| {
456
3
					let RotateOrbiterResult {
457
3
						maybe_old_orbiter,
458
3
						maybe_next_orbiter,
459
3
					} = pool.rotate_orbiter();
460

            
461
					// remove old orbiter, if any.
462
					if let Some(CurrentOrbiter {
463
2
						account_id: ref current_orbiter,
464
2
						removed,
465
3
					}) = maybe_old_orbiter
466
					{
467
2
						if removed {
468
							Self::deposit_event(Event::OrbiterLeaveCollatorPool {
469
								collator: collator.clone(),
470
								orbiter: current_orbiter.clone(),
471
							});
472
2
						}
473
2
						AccountLookupOverride::<T>::remove(current_orbiter.clone());
474
2
						writes += 1;
475
1
					}
476
3
					if let Some(next_orbiter) = maybe_next_orbiter {
477
						// Forbidding the collator to write blocks, it is now up to its orbiters to do it.
478
3
						AccountLookupOverride::<T>::insert(
479
3
							collator.clone(),
480
3
							Option::<T::AccountId>::None,
481
3
						);
482
3
						writes += 1;
483
3

            
484
3
						// Insert new current orbiter
485
3
						AccountLookupOverride::<T>::insert(
486
3
							next_orbiter.clone(),
487
3
							Some(collator.clone()),
488
3
						);
489
3
						writes += 1;
490
3

            
491
3
						let mut i = Zero::zero();
492
9
						while i < T::RotatePeriod::get() {
493
6
							OrbiterPerRound::<T>::insert(
494
6
								round_index.saturating_add(i),
495
6
								collator.clone(),
496
6
								next_orbiter.clone(),
497
6
							);
498
6
							i += One::one();
499
6
							writes += 1;
500
6
						}
501

            
502
3
						Self::deposit_event(Event::OrbiterRotation {
503
3
							collator,
504
3
							old_orbiter: maybe_old_orbiter.map(|orbiter| orbiter.account_id),
505
3
							new_orbiter: Some(next_orbiter),
506
3
						});
507
					} else {
508
						// If there is no more active orbiter, you have to remove the collator override.
509
						AccountLookupOverride::<T>::remove(collator.clone());
510
						writes += 1;
511
						Self::deposit_event(Event::OrbiterRotation {
512
							collator,
513
							old_orbiter: maybe_old_orbiter.map(|orbiter| orbiter.account_id),
514
							new_orbiter: None,
515
						});
516
					}
517
3
					writes += 1;
518
3
					Some(pool)
519
59
				},
520
59
			);
521
59
			T::DbWeight::get().reads_writes(1, writes)
522
59
		}
523
		/// Notify this pallet that a new round begin
524
110
		pub fn on_new_round(round_index: T::RoundIndex) -> Weight {
525
110
			CurrentRound::<T>::put(round_index);
526
110

            
527
110
			if ForceRotation::<T>::get() {
528
				ForceRotation::<T>::put(false);
529
				let _ = Self::on_rotate(round_index);
530
				T::WeightInfo::on_new_round()
531
110
			} else if round_index % T::RotatePeriod::get() == Zero::zero() {
532
59
				let _ = Self::on_rotate(round_index);
533
59
				T::WeightInfo::on_new_round()
534
			} else {
535
51
				T::DbWeight::get().writes(1)
536
			}
537
110
		}
538
		/// Notify this pallet that a collator received rewards
539
		pub fn distribute_rewards(
540
			pay_for_round: T::RoundIndex,
541
			collator: T::AccountId,
542
			amount: BalanceOf<T>,
543
		) -> Weight {
544
			if let Some(orbiter) = OrbiterPerRound::<T>::take(pay_for_round, &collator) {
545
				if T::Currency::deposit_into_existing(&orbiter, amount).is_ok() {
546
					Self::deposit_event(Event::OrbiterRewarded {
547
						account: orbiter,
548
						rewards: amount,
549
					});
550
				}
551
				T::WeightInfo::distribute_rewards()
552
			} else {
553
				// writes: take
554
				T::DbWeight::get().writes(1)
555
			}
556
		}
557

            
558
		/// Check if an account is a collator pool account with an
559
		/// orbiter assigned for a given round
560
44
		pub fn is_collator_pool_with_active_orbiter(
561
44
			for_round: T::RoundIndex,
562
44
			collator: T::AccountId,
563
44
		) -> bool {
564
44
			OrbiterPerRound::<T>::contains_key(for_round, &collator)
565
44
		}
566
	}
567
}
568

            
569
impl<T: Config> AccountLookup<T::AccountId> for Pallet<T> {
570
58896
	fn lookup_account(nimbus_id: &NimbusId) -> Option<T::AccountId> {
571
58896
		let account_id = T::AccountLookup::lookup_account(nimbus_id)?;
572
58896
		match AccountLookupOverride::<T>::get(&account_id) {
573
			Some(override_) => override_,
574
58896
			None => Some(account_id),
575
		}
576
58896
	}
577
}