pezkuwi_runtime_common/slots/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
2// This file is part of Pezkuwi.
3
4// Pezkuwi 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// Pezkuwi 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 Pezkuwi.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Parathread and teyrchains leasing system. Allows para IDs to be claimed, the code and data to be
18//! initialized and teyrchain slots (i.e. continuous scheduling) to be leased. Also allows for
19//! teyrchains and parathreads to be swapped.
20//!
21//! This doesn't handle the mechanics of determining which para ID actually ends up with a teyrchain
22//! lease. This must handled by a separately, through the trait interface that this pezpallet
23//! provides or the root dispatchables.
24
25pub mod migration;
26
27use crate::traits::{LeaseError, Leaser, Registrar};
28use alloc::vec::Vec;
29use pezframe_support::{
30	pezpallet_prelude::*,
31	traits::{Currency, ReservableCurrency},
32	weights::Weight,
33};
34use pezframe_system::pezpallet_prelude::*;
35use pezkuwi_primitives::Id as ParaId;
36pub use pezpallet::*;
37use pezsp_runtime::traits::{CheckedConversion, CheckedSub, Saturating, Zero};
38
39type BalanceOf<T> =
40	<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
41type LeasePeriodOf<T> = BlockNumberFor<T>;
42
43pub trait WeightInfo {
44	fn force_lease() -> Weight;
45	fn manage_lease_period_start(c: u32, t: u32) -> Weight;
46	fn clear_all_leases() -> Weight;
47	fn trigger_onboard() -> Weight;
48}
49
50pub struct TestWeightInfo;
51impl WeightInfo for TestWeightInfo {
52	fn force_lease() -> Weight {
53		Weight::zero()
54	}
55	fn manage_lease_period_start(_c: u32, _t: u32) -> Weight {
56		Weight::zero()
57	}
58	fn clear_all_leases() -> Weight {
59		Weight::zero()
60	}
61	fn trigger_onboard() -> Weight {
62		Weight::zero()
63	}
64}
65
66#[pezframe_support::pezpallet]
67pub mod pezpallet {
68	use super::*;
69
70	#[pezpallet::pezpallet]
71	#[pezpallet::without_storage_info]
72	pub struct Pezpallet<T>(_);
73
74	#[pezpallet::config]
75	pub trait Config: pezframe_system::Config {
76		/// The overarching event type.
77		#[allow(deprecated)]
78		type RuntimeEvent: From<Event<Self>>
79			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
80
81		/// The currency type used for bidding.
82		type Currency: ReservableCurrency<Self::AccountId>;
83
84		/// The teyrchain registrar type.
85		type Registrar: Registrar<AccountId = Self::AccountId>;
86
87		/// The number of blocks over which a single period lasts.
88		#[pezpallet::constant]
89		type LeasePeriod: Get<BlockNumberFor<Self>>;
90
91		/// The number of blocks to offset each lease period by.
92		#[pezpallet::constant]
93		type LeaseOffset: Get<BlockNumberFor<Self>>;
94
95		/// The origin which may forcibly create or clear leases. Root can always do this.
96		type ForceOrigin: EnsureOrigin<<Self as pezframe_system::Config>::RuntimeOrigin>;
97
98		/// Weight Information for the Extrinsics in the Pezpallet
99		type WeightInfo: WeightInfo;
100	}
101
102	/// Amounts held on deposit for each (possibly future) leased teyrchain.
103	///
104	/// The actual amount locked on its behalf by any account at any time is the maximum of the
105	/// second values of the items in this list whose first value is the account.
106	///
107	/// The first item in the list is the amount locked for the current Lease Period. Following
108	/// items are for the subsequent lease periods.
109	///
110	/// The default value (an empty list) implies that the teyrchain no longer exists (or never
111	/// existed) as far as this pezpallet is concerned.
112	///
113	/// If a teyrchain doesn't exist *yet* but is scheduled to exist in the future, then it
114	/// will be left-padded with one or more `None`s to denote the fact that nothing is held on
115	/// deposit for the non-existent chain currently, but is held at some point in the future.
116	///
117	/// It is illegal for a `None` value to trail in the list.
118	#[pezpallet::storage]
119	pub type Leases<T: Config> =
120		StorageMap<_, Twox64Concat, ParaId, Vec<Option<(T::AccountId, BalanceOf<T>)>>, ValueQuery>;
121
122	#[pezpallet::event]
123	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
124	pub enum Event<T: Config> {
125		/// A new `[lease_period]` is beginning.
126		NewLeasePeriod { lease_period: LeasePeriodOf<T> },
127		/// A para has won the right to a continuous set of lease periods as a teyrchain.
128		/// First balance is any extra amount reserved on top of the para's existing deposit.
129		/// Second balance is the total amount reserved.
130		Leased {
131			para_id: ParaId,
132			leaser: T::AccountId,
133			period_begin: LeasePeriodOf<T>,
134			period_count: LeasePeriodOf<T>,
135			extra_reserved: BalanceOf<T>,
136			total_amount: BalanceOf<T>,
137		},
138	}
139
140	#[pezpallet::error]
141	pub enum Error<T> {
142		/// The teyrchain ID is not onboarding.
143		ParaNotOnboarding,
144		/// There was an error with the lease.
145		LeaseError,
146	}
147
148	#[pezpallet::hooks]
149	impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
150		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
151			if let Some((lease_period, first_block)) = Self::lease_period_index(n) {
152				// If we're beginning a new lease period then handle that.
153				if first_block {
154					return Self::manage_lease_period_start(lease_period);
155				}
156			}
157
158			// We didn't return early above, so we didn't do anything.
159			Weight::zero()
160		}
161	}
162
163	#[pezpallet::call]
164	impl<T: Config> Pezpallet<T> {
165		/// Just a connect into the `lease_out` call, in case Root wants to force some lease to
166		/// happen independently of any other on-chain mechanism to use it.
167		///
168		/// The dispatch origin for this call must match `T::ForceOrigin`.
169		#[pezpallet::call_index(0)]
170		#[pezpallet::weight(T::WeightInfo::force_lease())]
171		pub fn force_lease(
172			origin: OriginFor<T>,
173			para: ParaId,
174			leaser: T::AccountId,
175			amount: BalanceOf<T>,
176			period_begin: LeasePeriodOf<T>,
177			period_count: LeasePeriodOf<T>,
178		) -> DispatchResult {
179			T::ForceOrigin::ensure_origin(origin)?;
180			Self::lease_out(para, &leaser, amount, period_begin, period_count)
181				.map_err(|_| Error::<T>::LeaseError)?;
182			Ok(())
183		}
184
185		/// Clear all leases for a Para Id, refunding any deposits back to the original owners.
186		///
187		/// The dispatch origin for this call must match `T::ForceOrigin`.
188		#[pezpallet::call_index(1)]
189		#[pezpallet::weight(T::WeightInfo::clear_all_leases())]
190		pub fn clear_all_leases(origin: OriginFor<T>, para: ParaId) -> DispatchResult {
191			T::ForceOrigin::ensure_origin(origin)?;
192			let deposits = Self::all_deposits_held(para);
193
194			// Refund any deposits for these leases
195			for (who, deposit) in deposits {
196				let err_amount = T::Currency::unreserve(&who, deposit);
197				debug_assert!(err_amount.is_zero());
198			}
199
200			Leases::<T>::remove(para);
201			Ok(())
202		}
203
204		/// Try to onboard a teyrchain that has a lease for the current lease period.
205		///
206		/// This function can be useful if there was some state issue with a para that should
207		/// have onboarded, but was unable to. As long as they have a lease period, we can
208		/// let them onboard from here.
209		///
210		/// Origin must be signed, but can be called by anyone.
211		#[pezpallet::call_index(2)]
212		#[pezpallet::weight(T::WeightInfo::trigger_onboard())]
213		pub fn trigger_onboard(origin: OriginFor<T>, para: ParaId) -> DispatchResult {
214			ensure_signed(origin)?;
215			let leases = Leases::<T>::get(para);
216			match leases.first() {
217				// If the first element in leases is present, then it has a lease!
218				// We can try to onboard it.
219				Some(Some(_lease_info)) => T::Registrar::make_teyrchain(para)?,
220				// Otherwise, it does not have a lease.
221				Some(None) | None => return Err(Error::<T>::ParaNotOnboarding.into()),
222			};
223			Ok(())
224		}
225	}
226}
227
228impl<T: Config> Pezpallet<T> {
229	/// A new lease period is beginning. We're at the start of the first block of it.
230	///
231	/// We need to on-board and off-board teyrchains as needed. We should also handle reducing/
232	/// returning deposits.
233	fn manage_lease_period_start(lease_period_index: LeasePeriodOf<T>) -> Weight {
234		Self::deposit_event(Event::<T>::NewLeasePeriod { lease_period: lease_period_index });
235
236		let old_teyrchains = T::Registrar::teyrchains();
237
238		// Figure out what chains need bringing on.
239		let mut teyrchains = Vec::new();
240		for (para, mut lease_periods) in Leases::<T>::iter() {
241			if lease_periods.is_empty() {
242				continue;
243			}
244			// ^^ should never be empty since we would have deleted the entry otherwise.
245
246			if lease_periods.len() == 1 {
247				// Just one entry, which corresponds to the now-ended lease period.
248				//
249				// `para` is now just an on-demand teyrchain.
250				//
251				// Unreserve whatever is left.
252				if let Some((who, value)) = &lease_periods[0] {
253					T::Currency::unreserve(&who, *value);
254				}
255
256				// Remove the now-empty lease list.
257				Leases::<T>::remove(para);
258			} else {
259				// The teyrchain entry has leased future periods.
260
261				// We need to pop the first deposit entry, which corresponds to the now-
262				// ended lease period.
263				let maybe_ended_lease = lease_periods.remove(0);
264
265				Leases::<T>::insert(para, &lease_periods);
266
267				// If we *were* active in the last period and so have ended a lease...
268				if let Some(ended_lease) = maybe_ended_lease {
269					// Then we need to get the new amount that should continue to be held on
270					// deposit for the teyrchain.
271					let now_held = Self::deposit_held(para, &ended_lease.0);
272
273					// If this is less than what we were holding for this leaser's now-ended lease,
274					// then unreserve it.
275					if let Some(rebate) = ended_lease.1.checked_sub(&now_held) {
276						T::Currency::unreserve(&ended_lease.0, rebate);
277					}
278				}
279
280				// If we have an active lease in the new period, then add to the current teyrchains
281				if lease_periods[0].is_some() {
282					teyrchains.push(para);
283				}
284			}
285		}
286		teyrchains.sort();
287
288		for para in teyrchains.iter() {
289			if old_teyrchains.binary_search(para).is_err() {
290				// incoming.
291				let res = T::Registrar::make_teyrchain(*para);
292				debug_assert!(res.is_ok());
293			}
294		}
295
296		for para in old_teyrchains.iter() {
297			if teyrchains.binary_search(para).is_err() {
298				// outgoing.
299				let res = T::Registrar::make_parathread(*para);
300				debug_assert!(res.is_ok());
301			}
302		}
303
304		T::WeightInfo::manage_lease_period_start(
305			old_teyrchains.len() as u32,
306			teyrchains.len() as u32,
307		)
308	}
309
310	// Return a vector of (user, balance) for all deposits for a teyrchain.
311	// Useful when trying to clean up a teyrchain leases, as this would tell
312	// you all the balances you need to unreserve.
313	fn all_deposits_held(para: ParaId) -> Vec<(T::AccountId, BalanceOf<T>)> {
314		let mut tracker = alloc::collections::btree_map::BTreeMap::new();
315		Leases::<T>::get(para).into_iter().for_each(|lease| match lease {
316			Some((who, amount)) => match tracker.get(&who) {
317				Some(prev_amount) => {
318					if amount > *prev_amount {
319						tracker.insert(who, amount);
320					}
321				},
322				None => {
323					tracker.insert(who, amount);
324				},
325			},
326			None => {},
327		});
328
329		tracker.into_iter().collect()
330	}
331}
332
333impl<T: Config> crate::traits::OnSwap for Pezpallet<T> {
334	fn on_swap(one: ParaId, other: ParaId) {
335		Leases::<T>::mutate(one, |x| Leases::<T>::mutate(other, |y| core::mem::swap(x, y)))
336	}
337}
338
339impl<T: Config> Leaser<BlockNumberFor<T>> for Pezpallet<T> {
340	type AccountId = T::AccountId;
341	type LeasePeriod = BlockNumberFor<T>;
342	type Currency = T::Currency;
343
344	fn lease_out(
345		para: ParaId,
346		leaser: &Self::AccountId,
347		amount: <Self::Currency as Currency<Self::AccountId>>::Balance,
348		period_begin: Self::LeasePeriod,
349		period_count: Self::LeasePeriod,
350	) -> Result<(), LeaseError> {
351		let now = pezframe_system::Pezpallet::<T>::block_number();
352		let (current_lease_period, _) =
353			Self::lease_period_index(now).ok_or(LeaseError::NoLeasePeriod)?;
354		// Finally, we update the deposit held so it is `amount` for the new lease period
355		// indices that were won in the auction.
356		let offset = period_begin
357			.checked_sub(&current_lease_period)
358			.and_then(|x| x.checked_into::<usize>())
359			.ok_or(LeaseError::AlreadyEnded)?;
360
361		// offset is the amount into the `Deposits` items list that our lease begins. `period_count`
362		// is the number of items that it lasts for.
363
364		// The lease period index range (begin, end) that newly belongs to this teyrchain
365		// ID. We need to ensure that it features in `Deposits` to prevent it from being
366		// reaped too early (any managed teyrchain whose `Deposits` set runs low will be
367		// removed).
368		Leases::<T>::try_mutate(para, |d| {
369			// Left-pad with `None`s as necessary.
370			if d.len() < offset {
371				d.resize_with(offset, || None);
372			}
373			let period_count_usize =
374				period_count.checked_into::<usize>().ok_or(LeaseError::AlreadyEnded)?;
375			// Then place the deposit values for as long as the chain should exist.
376			for i in offset..(offset + period_count_usize) {
377				if d.len() > i {
378					// Already exists but it's `None`. That means a later slot was already leased.
379					// No problem.
380					if d[i] == None {
381						d[i] = Some((leaser.clone(), amount));
382					} else {
383						// The chain tried to lease the same period twice. This might be a griefing
384						// attempt.
385						//
386						// We bail, not giving any lease and leave it for governance to sort out.
387						return Err(LeaseError::AlreadyLeased);
388					}
389				} else if d.len() == i {
390					// Doesn't exist. This is usual.
391					d.push(Some((leaser.clone(), amount)));
392				} else {
393					// earlier resize means it must be >= i; qed
394					// defensive code though since we really don't want to panic here.
395				}
396			}
397
398			// Figure out whether we already have some funds of `leaser` held in reserve for
399			// `para_id`.  If so, then we can deduct those from the amount that we need to reserve.
400			let maybe_additional = amount.checked_sub(&Self::deposit_held(para, &leaser));
401			if let Some(ref additional) = maybe_additional {
402				T::Currency::reserve(&leaser, *additional)
403					.map_err(|_| LeaseError::ReserveFailed)?;
404			}
405
406			let reserved = maybe_additional.unwrap_or_default();
407
408			// Check if current lease period is same as period begin, and onboard them directly.
409			// This will allow us to support onboarding new teyrchains in the middle of a lease
410			// period.
411			if current_lease_period == period_begin {
412				// Best effort. Not much we can do if this fails.
413				let _ = T::Registrar::make_teyrchain(para);
414			}
415
416			Self::deposit_event(Event::<T>::Leased {
417				para_id: para,
418				leaser: leaser.clone(),
419				period_begin,
420				period_count,
421				extra_reserved: reserved,
422				total_amount: amount,
423			});
424
425			Ok(())
426		})
427	}
428
429	fn deposit_held(
430		para: ParaId,
431		leaser: &Self::AccountId,
432	) -> <Self::Currency as Currency<Self::AccountId>>::Balance {
433		Leases::<T>::get(para)
434			.into_iter()
435			.map(|lease| match lease {
436				Some((who, amount)) => {
437					if &who == leaser {
438						amount
439					} else {
440						Zero::zero()
441					}
442				},
443				None => Zero::zero(),
444			})
445			.max()
446			.unwrap_or_else(Zero::zero)
447	}
448
449	#[cfg(any(feature = "runtime-benchmarks", test))]
450	fn lease_period_length() -> (BlockNumberFor<T>, BlockNumberFor<T>) {
451		(T::LeasePeriod::get(), T::LeaseOffset::get())
452	}
453
454	fn lease_period_index(b: BlockNumberFor<T>) -> Option<(Self::LeasePeriod, bool)> {
455		// Note that blocks before `LeaseOffset` do not count as any lease period.
456		let offset_block_now = b.checked_sub(&T::LeaseOffset::get())?;
457		let lease_period = offset_block_now / T::LeasePeriod::get();
458		let at_begin = (offset_block_now % T::LeasePeriod::get()).is_zero();
459
460		Some((lease_period, at_begin))
461	}
462
463	fn already_leased(
464		para_id: ParaId,
465		first_period: Self::LeasePeriod,
466		last_period: Self::LeasePeriod,
467	) -> bool {
468		let now = pezframe_system::Pezpallet::<T>::block_number();
469		let (current_lease_period, _) = match Self::lease_period_index(now) {
470			Some(clp) => clp,
471			None => return true,
472		};
473
474		// Can't look in the past, so we pick whichever is the biggest.
475		let start_period = first_period.max(current_lease_period);
476		// Find the offset to look into the lease period list.
477		// Subtraction is safe because of max above.
478		let offset = match (start_period - current_lease_period).checked_into::<usize>() {
479			Some(offset) => offset,
480			None => return true,
481		};
482
483		// This calculates how deep we should look in the vec for a potential lease.
484		let period_count = match last_period.saturating_sub(start_period).checked_into::<usize>() {
485			Some(period_count) => period_count,
486			None => return true,
487		};
488
489		// Get the leases, and check each item in the vec which is part of the range we are
490		// checking.
491		let leases = Leases::<T>::get(para_id);
492		for slot in offset..=offset + period_count {
493			if let Some(Some(_)) = leases.get(slot) {
494				// If there exists any lease period, we exit early and return true.
495				return true;
496			}
497		}
498
499		// If we got here, then we did not find any overlapping leases.
500		false
501	}
502}
503
504/// tests for this pezpallet
505#[cfg(test)]
506mod tests {
507	use super::*;
508
509	use crate::{mock::TestRegistrar, slots};
510	use pezframe_support::{assert_noop, assert_ok, derive_impl, parameter_types};
511	use pezframe_system::EnsureRoot;
512	use pezkuwi_primitives::BlockNumber;
513	use pezkuwi_primitives_test_helpers::{dummy_head_data, dummy_validation_code};
514	use pezpallet_balances;
515	use pezsp_core::H256;
516	use pezsp_runtime::{
517		traits::{BlakeTwo256, IdentityLookup},
518		BuildStorage,
519	};
520
521	type Block = pezframe_system::mocking::MockBlockU32<Test>;
522
523	pezframe_support::construct_runtime!(
524		pub enum Test
525		{
526			System: pezframe_system,
527			Balances: pezpallet_balances,
528			Slots: slots,
529		}
530	);
531
532	#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
533	impl pezframe_system::Config for Test {
534		type BaseCallFilter = pezframe_support::traits::Everything;
535		type BlockWeights = ();
536		type BlockLength = ();
537		type RuntimeOrigin = RuntimeOrigin;
538		type RuntimeCall = RuntimeCall;
539		type Nonce = u64;
540		type Hash = H256;
541		type Hashing = BlakeTwo256;
542		type AccountId = u64;
543		type Lookup = IdentityLookup<Self::AccountId>;
544		type Block = Block;
545		type RuntimeEvent = RuntimeEvent;
546		type DbWeight = ();
547		type Version = ();
548		type PalletInfo = PalletInfo;
549		type AccountData = pezpallet_balances::AccountData<u64>;
550		type OnNewAccount = ();
551		type OnKilledAccount = ();
552		type SystemWeightInfo = ();
553		type SS58Prefix = ();
554		type OnSetCode = ();
555		type MaxConsumers = pezframe_support::traits::ConstU32<16>;
556	}
557
558	#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
559	impl pezpallet_balances::Config for Test {
560		type AccountStore = System;
561	}
562
563	parameter_types! {
564		pub const LeasePeriod: BlockNumber = 10;
565		pub static LeaseOffset: BlockNumber = 0;
566		pub const ParaDeposit: u64 = 1;
567	}
568
569	impl Config for Test {
570		type RuntimeEvent = RuntimeEvent;
571		type Currency = Balances;
572		type Registrar = TestRegistrar<Test>;
573		type LeasePeriod = LeasePeriod;
574		type LeaseOffset = LeaseOffset;
575		type ForceOrigin = EnsureRoot<Self::AccountId>;
576		type WeightInfo = crate::slots::TestWeightInfo;
577	}
578
579	// This function basically just builds a genesis storage key/value store according to
580	// our desired mock up.
581	pub fn new_test_ext() -> pezsp_io::TestExternalities {
582		let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
583		pezpallet_balances::GenesisConfig::<Test> {
584			balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)],
585			..Default::default()
586		}
587		.assimilate_storage(&mut t)
588		.unwrap();
589		t.into()
590	}
591
592	#[test]
593	fn basic_setup_works() {
594		new_test_ext().execute_with(|| {
595			System::run_to_block::<AllPalletsWithSystem>(1);
596			assert_eq!(Slots::lease_period_length(), (10, 0));
597			let now = System::block_number();
598			assert_eq!(Slots::lease_period_index(now).unwrap().0, 0);
599			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
600
601			System::run_to_block::<AllPalletsWithSystem>(10);
602			let now = System::block_number();
603			assert_eq!(Slots::lease_period_index(now).unwrap().0, 1);
604		});
605	}
606
607	#[test]
608	fn lease_lifecycle_works() {
609		new_test_ext().execute_with(|| {
610			System::run_to_block::<AllPalletsWithSystem>(1);
611
612			assert_ok!(TestRegistrar::<Test>::register(
613				1,
614				ParaId::from(1_u32),
615				dummy_head_data(),
616				dummy_validation_code()
617			));
618
619			assert_ok!(Slots::lease_out(1.into(), &1, 1, 1, 1));
620			assert_eq!(Slots::deposit_held(1.into(), &1), 1);
621			assert_eq!(Balances::reserved_balance(1), 1);
622
623			System::run_to_block::<AllPalletsWithSystem>(19);
624			assert_eq!(Slots::deposit_held(1.into(), &1), 1);
625			assert_eq!(Balances::reserved_balance(1), 1);
626
627			System::run_to_block::<AllPalletsWithSystem>(20);
628			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
629			assert_eq!(Balances::reserved_balance(1), 0);
630
631			assert_eq!(
632				TestRegistrar::<Test>::operations(),
633				vec![(1.into(), 10, true), (1.into(), 20, false),]
634			);
635		});
636	}
637
638	#[test]
639	fn lease_interrupted_lifecycle_works() {
640		new_test_ext().execute_with(|| {
641			System::run_to_block::<AllPalletsWithSystem>(1);
642
643			assert_ok!(TestRegistrar::<Test>::register(
644				1,
645				ParaId::from(1_u32),
646				dummy_head_data(),
647				dummy_validation_code()
648			));
649
650			assert_ok!(Slots::lease_out(1.into(), &1, 6, 1, 1));
651			assert_ok!(Slots::lease_out(1.into(), &1, 4, 3, 1));
652
653			System::run_to_block::<AllPalletsWithSystem>(19);
654			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
655			assert_eq!(Balances::reserved_balance(1), 6);
656
657			System::run_to_block::<AllPalletsWithSystem>(20);
658			assert_eq!(Slots::deposit_held(1.into(), &1), 4);
659			assert_eq!(Balances::reserved_balance(1), 4);
660
661			System::run_to_block::<AllPalletsWithSystem>(39);
662			assert_eq!(Slots::deposit_held(1.into(), &1), 4);
663			assert_eq!(Balances::reserved_balance(1), 4);
664
665			System::run_to_block::<AllPalletsWithSystem>(40);
666			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
667			assert_eq!(Balances::reserved_balance(1), 0);
668
669			assert_eq!(
670				TestRegistrar::<Test>::operations(),
671				vec![
672					(1.into(), 10, true),
673					(1.into(), 20, false),
674					(1.into(), 30, true),
675					(1.into(), 40, false),
676				]
677			);
678		});
679	}
680
681	#[test]
682	fn lease_relayed_lifecycle_works() {
683		new_test_ext().execute_with(|| {
684			System::run_to_block::<AllPalletsWithSystem>(1);
685
686			assert_ok!(TestRegistrar::<Test>::register(
687				1,
688				ParaId::from(1_u32),
689				dummy_head_data(),
690				dummy_validation_code()
691			));
692
693			assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok());
694			assert!(Slots::lease_out(1.into(), &2, 4, 2, 1).is_ok());
695			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
696			assert_eq!(Balances::reserved_balance(1), 6);
697			assert_eq!(Slots::deposit_held(1.into(), &2), 4);
698			assert_eq!(Balances::reserved_balance(2), 4);
699
700			System::run_to_block::<AllPalletsWithSystem>(19);
701			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
702			assert_eq!(Balances::reserved_balance(1), 6);
703			assert_eq!(Slots::deposit_held(1.into(), &2), 4);
704			assert_eq!(Balances::reserved_balance(2), 4);
705
706			System::run_to_block::<AllPalletsWithSystem>(20);
707			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
708			assert_eq!(Balances::reserved_balance(1), 0);
709			assert_eq!(Slots::deposit_held(1.into(), &2), 4);
710			assert_eq!(Balances::reserved_balance(2), 4);
711
712			System::run_to_block::<AllPalletsWithSystem>(29);
713			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
714			assert_eq!(Balances::reserved_balance(1), 0);
715			assert_eq!(Slots::deposit_held(1.into(), &2), 4);
716			assert_eq!(Balances::reserved_balance(2), 4);
717
718			System::run_to_block::<AllPalletsWithSystem>(30);
719			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
720			assert_eq!(Balances::reserved_balance(1), 0);
721			assert_eq!(Slots::deposit_held(1.into(), &2), 0);
722			assert_eq!(Balances::reserved_balance(2), 0);
723
724			assert_eq!(
725				TestRegistrar::<Test>::operations(),
726				vec![(1.into(), 10, true), (1.into(), 30, false),]
727			);
728		});
729	}
730
731	#[test]
732	fn lease_deposit_increase_works() {
733		new_test_ext().execute_with(|| {
734			System::run_to_block::<AllPalletsWithSystem>(1);
735
736			assert_ok!(TestRegistrar::<Test>::register(
737				1,
738				ParaId::from(1_u32),
739				dummy_head_data(),
740				dummy_validation_code()
741			));
742
743			assert!(Slots::lease_out(1.into(), &1, 4, 1, 1).is_ok());
744			assert_eq!(Slots::deposit_held(1.into(), &1), 4);
745			assert_eq!(Balances::reserved_balance(1), 4);
746
747			assert!(Slots::lease_out(1.into(), &1, 6, 2, 1).is_ok());
748			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
749			assert_eq!(Balances::reserved_balance(1), 6);
750
751			System::run_to_block::<AllPalletsWithSystem>(29);
752			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
753			assert_eq!(Balances::reserved_balance(1), 6);
754
755			System::run_to_block::<AllPalletsWithSystem>(30);
756			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
757			assert_eq!(Balances::reserved_balance(1), 0);
758
759			assert_eq!(
760				TestRegistrar::<Test>::operations(),
761				vec![(1.into(), 10, true), (1.into(), 30, false),]
762			);
763		});
764	}
765
766	#[test]
767	fn lease_deposit_decrease_works() {
768		new_test_ext().execute_with(|| {
769			System::run_to_block::<AllPalletsWithSystem>(1);
770
771			assert_ok!(TestRegistrar::<Test>::register(
772				1,
773				ParaId::from(1_u32),
774				dummy_head_data(),
775				dummy_validation_code()
776			));
777
778			assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok());
779			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
780			assert_eq!(Balances::reserved_balance(1), 6);
781
782			assert!(Slots::lease_out(1.into(), &1, 4, 2, 1).is_ok());
783			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
784			assert_eq!(Balances::reserved_balance(1), 6);
785
786			System::run_to_block::<AllPalletsWithSystem>(19);
787			assert_eq!(Slots::deposit_held(1.into(), &1), 6);
788			assert_eq!(Balances::reserved_balance(1), 6);
789
790			System::run_to_block::<AllPalletsWithSystem>(20);
791			assert_eq!(Slots::deposit_held(1.into(), &1), 4);
792			assert_eq!(Balances::reserved_balance(1), 4);
793
794			System::run_to_block::<AllPalletsWithSystem>(29);
795			assert_eq!(Slots::deposit_held(1.into(), &1), 4);
796			assert_eq!(Balances::reserved_balance(1), 4);
797
798			System::run_to_block::<AllPalletsWithSystem>(30);
799			assert_eq!(Slots::deposit_held(1.into(), &1), 0);
800			assert_eq!(Balances::reserved_balance(1), 0);
801
802			assert_eq!(
803				TestRegistrar::<Test>::operations(),
804				vec![(1.into(), 10, true), (1.into(), 30, false),]
805			);
806		});
807	}
808
809	#[test]
810	fn clear_all_leases_works() {
811		new_test_ext().execute_with(|| {
812			System::run_to_block::<AllPalletsWithSystem>(1);
813
814			assert_ok!(TestRegistrar::<Test>::register(
815				1,
816				ParaId::from(1_u32),
817				dummy_head_data(),
818				dummy_validation_code()
819			));
820
821			let max_num = 5u32;
822
823			// max_num different people are reserved for leases to Para ID 1
824			for i in 1u32..=max_num {
825				let j: u64 = i.into();
826				assert_ok!(Slots::lease_out(1.into(), &j, j * 10 - 1, i * i, i));
827				assert_eq!(Slots::deposit_held(1.into(), &j), j * 10 - 1);
828				assert_eq!(Balances::reserved_balance(j), j * 10 - 1);
829			}
830
831			assert_ok!(Slots::clear_all_leases(RuntimeOrigin::root(), 1.into()));
832
833			// Balances cleaned up correctly
834			for i in 1u32..=max_num {
835				let j: u64 = i.into();
836				assert_eq!(Slots::deposit_held(1.into(), &j), 0);
837				assert_eq!(Balances::reserved_balance(j), 0);
838			}
839
840			// Leases is empty.
841			assert!(Leases::<Test>::get(ParaId::from(1_u32)).is_empty());
842		});
843	}
844
845	#[test]
846	fn lease_out_current_lease_period() {
847		new_test_ext().execute_with(|| {
848			System::run_to_block::<AllPalletsWithSystem>(1);
849
850			assert_ok!(TestRegistrar::<Test>::register(
851				1,
852				ParaId::from(1_u32),
853				dummy_head_data(),
854				dummy_validation_code()
855			));
856			assert_ok!(TestRegistrar::<Test>::register(
857				1,
858				ParaId::from(2_u32),
859				dummy_head_data(),
860				dummy_validation_code()
861			));
862
863			System::run_to_block::<AllPalletsWithSystem>(20);
864			let now = System::block_number();
865			assert_eq!(Slots::lease_period_index(now).unwrap().0, 2);
866			// Can't lease from the past
867			assert!(Slots::lease_out(1.into(), &1, 1, 1, 1).is_err());
868			// Lease in the current period triggers onboarding
869			assert_ok!(Slots::lease_out(1.into(), &1, 1, 2, 1));
870			// Lease in the future doesn't
871			assert_ok!(Slots::lease_out(2.into(), &1, 1, 3, 1));
872
873			assert_eq!(TestRegistrar::<Test>::operations(), vec![(1.into(), 20, true),]);
874		});
875	}
876
877	#[test]
878	fn trigger_onboard_works() {
879		new_test_ext().execute_with(|| {
880			System::run_to_block::<AllPalletsWithSystem>(1);
881			assert_ok!(TestRegistrar::<Test>::register(
882				1,
883				ParaId::from(1_u32),
884				dummy_head_data(),
885				dummy_validation_code()
886			));
887			assert_ok!(TestRegistrar::<Test>::register(
888				1,
889				ParaId::from(2_u32),
890				dummy_head_data(),
891				dummy_validation_code()
892			));
893			assert_ok!(TestRegistrar::<Test>::register(
894				1,
895				ParaId::from(3_u32),
896				dummy_head_data(),
897				dummy_validation_code()
898			));
899
900			// We will directly manipulate leases to emulate some kind of failure in the system.
901			// Para 1 will have no leases
902			// Para 2 will have a lease period in the current index
903			Leases::<Test>::insert(ParaId::from(2_u32), vec![Some((0, 0))]);
904			// Para 3 will have a lease period in a future index
905			Leases::<Test>::insert(ParaId::from(3_u32), vec![None, None, Some((0, 0))]);
906
907			// Para 1 should fail cause they don't have any leases
908			assert_noop!(
909				Slots::trigger_onboard(RuntimeOrigin::signed(1), 1.into()),
910				Error::<Test>::ParaNotOnboarding
911			);
912
913			// Para 2 should succeed
914			assert_ok!(Slots::trigger_onboard(RuntimeOrigin::signed(1), 2.into()));
915
916			// Para 3 should fail cause their lease is in the future
917			assert_noop!(
918				Slots::trigger_onboard(RuntimeOrigin::signed(1), 3.into()),
919				Error::<Test>::ParaNotOnboarding
920			);
921
922			// Trying Para 2 again should fail cause they are not currently an on-demand teyrchain
923			assert!(Slots::trigger_onboard(RuntimeOrigin::signed(1), 2.into()).is_err());
924
925			assert_eq!(TestRegistrar::<Test>::operations(), vec![(2.into(), 1, true),]);
926		});
927	}
928
929	#[test]
930	fn lease_period_offset_works() {
931		new_test_ext().execute_with(|| {
932			let (lpl, offset) = Slots::lease_period_length();
933			assert_eq!(offset, 0);
934			assert_eq!(Slots::lease_period_index(0), Some((0, true)));
935			assert_eq!(Slots::lease_period_index(1), Some((0, false)));
936			assert_eq!(Slots::lease_period_index(lpl - 1), Some((0, false)));
937			assert_eq!(Slots::lease_period_index(lpl), Some((1, true)));
938			assert_eq!(Slots::lease_period_index(lpl + 1), Some((1, false)));
939			assert_eq!(Slots::lease_period_index(2 * lpl - 1), Some((1, false)));
940			assert_eq!(Slots::lease_period_index(2 * lpl), Some((2, true)));
941			assert_eq!(Slots::lease_period_index(2 * lpl + 1), Some((2, false)));
942
943			// Lease period is 10, and we add an offset of 5.
944			LeaseOffset::set(5);
945			let (lpl, offset) = Slots::lease_period_length();
946			assert_eq!(offset, 5);
947			assert_eq!(Slots::lease_period_index(0), None);
948			assert_eq!(Slots::lease_period_index(1), None);
949			assert_eq!(Slots::lease_period_index(offset), Some((0, true)));
950			assert_eq!(Slots::lease_period_index(lpl), Some((0, false)));
951			assert_eq!(Slots::lease_period_index(lpl - 1 + offset), Some((0, false)));
952			assert_eq!(Slots::lease_period_index(lpl + offset), Some((1, true)));
953			assert_eq!(Slots::lease_period_index(lpl + offset + 1), Some((1, false)));
954			assert_eq!(Slots::lease_period_index(2 * lpl - 1 + offset), Some((1, false)));
955			assert_eq!(Slots::lease_period_index(2 * lpl + offset), Some((2, true)));
956			assert_eq!(Slots::lease_period_index(2 * lpl + offset + 1), Some((2, false)));
957		});
958	}
959}
960
961#[cfg(feature = "runtime-benchmarks")]
962mod benchmarking {
963	use super::*;
964	use pezframe_support::assert_ok;
965	use pezframe_system::RawOrigin;
966	use pezkuwi_runtime_teyrchains::paras;
967	use pezsp_runtime::traits::{Bounded, One};
968
969	use pezframe_benchmarking::v2::*;
970
971	use crate::slots::Pezpallet as Slots;
972
973	fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
974		let events = pezframe_system::Pezpallet::<T>::events();
975		let system_event: <T as pezframe_system::Config>::RuntimeEvent = generic_event.into();
976		// compare to the last event record
977		let pezframe_system::EventRecord { event, .. } = &events[events.len() - 1];
978		assert_eq!(event, &system_event);
979	}
980
981	// Registers a parathread (on-demand teyrchain)
982	fn register_a_parathread<T: Config + paras::Config>(i: u32) -> (ParaId, T::AccountId) {
983		let para = ParaId::from(i);
984		let leaser: T::AccountId = account("leaser", i, 0);
985		T::Currency::make_free_balance_be(&leaser, BalanceOf::<T>::max_value());
986		let worst_head_data = T::Registrar::worst_head_data();
987		let worst_validation_code = T::Registrar::worst_validation_code();
988
989		assert_ok!(T::Registrar::register(
990			leaser.clone(),
991			para,
992			worst_head_data,
993			worst_validation_code.clone(),
994		));
995		assert_ok!(paras::Pezpallet::<T>::add_trusted_validation_code(
996			pezframe_system::Origin::<T>::Root.into(),
997			worst_validation_code,
998		));
999
1000		T::Registrar::execute_pending_transitions();
1001
1002		(para, leaser)
1003	}
1004
1005	#[benchmarks(
1006		where T: paras::Config,
1007	)]
1008
1009	mod benchmarks {
1010		use super::*;
1011		use alloc::vec;
1012
1013		#[benchmark]
1014		fn force_lease() -> Result<(), BenchmarkError> {
1015			// If there is an offset, we need to be on that block to be able to do lease things.
1016			pezframe_system::Pezpallet::<T>::set_block_number(T::LeaseOffset::get() + One::one());
1017			let para = ParaId::from(1337);
1018			let leaser: T::AccountId = account("leaser", 0, 0);
1019			T::Currency::make_free_balance_be(&leaser, BalanceOf::<T>::max_value());
1020			let amount = T::Currency::minimum_balance();
1021			let period_begin = 69u32.into();
1022			let period_count = 3u32.into();
1023			let origin =
1024				T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
1025
1026			#[extrinsic_call]
1027			_(origin as T::RuntimeOrigin, para, leaser.clone(), amount, period_begin, period_count);
1028
1029			assert_last_event::<T>(
1030				Event::<T>::Leased {
1031					para_id: para,
1032					leaser,
1033					period_begin,
1034					period_count,
1035					extra_reserved: amount,
1036					total_amount: amount,
1037				}
1038				.into(),
1039			);
1040
1041			Ok(())
1042		}
1043
1044		// Worst case scenario, T on-demand teyrchains onboard, and C lease holding teyrchains
1045		// offboard. Assume reasonable maximum of 100 paras at any time
1046		#[benchmark]
1047		fn manage_lease_period_start(
1048			c: Linear<0, 100>,
1049			t: Linear<0, 100>,
1050		) -> Result<(), BenchmarkError> {
1051			let period_begin = 1u32.into();
1052			let period_count = 4u32.into();
1053
1054			// If there is an offset, we need to be on that block to be able to do lease things.
1055			pezframe_system::Pezpallet::<T>::set_block_number(T::LeaseOffset::get() + One::one());
1056
1057			// Make T parathreads (on-demand teyrchains)
1058			let paras_info = (0..t).map(|i| register_a_parathread::<T>(i)).collect::<Vec<_>>();
1059
1060			T::Registrar::execute_pending_transitions();
1061
1062			// T on-demand teyrchains are upgrading to lease holding teyrchains
1063			for (para, leaser) in paras_info {
1064				let amount = T::Currency::minimum_balance();
1065				let origin = T::ForceOrigin::try_successful_origin()
1066					.expect("ForceOrigin has no successful origin required for the benchmark");
1067				Slots::<T>::force_lease(origin, para, leaser, amount, period_begin, period_count)?;
1068			}
1069
1070			T::Registrar::execute_pending_transitions();
1071
1072			// C lease holding teyrchains are downgrading to on-demand teyrchains
1073			for i in 200..200 + c {
1074				let (para, _) = register_a_parathread::<T>(i);
1075				T::Registrar::make_teyrchain(para)?;
1076			}
1077
1078			T::Registrar::execute_pending_transitions();
1079
1080			for i in 0..t {
1081				assert!(T::Registrar::is_parathread(ParaId::from(i)));
1082			}
1083
1084			for i in 200..200 + c {
1085				assert!(T::Registrar::is_teyrchain(ParaId::from(i)));
1086			}
1087			#[block]
1088			{
1089				let _ = Slots::<T>::manage_lease_period_start(period_begin);
1090			}
1091
1092			// All paras should have switched.
1093			T::Registrar::execute_pending_transitions();
1094			for i in 0..t {
1095				assert!(T::Registrar::is_teyrchain(ParaId::from(i)));
1096			}
1097			for i in 200..200 + c {
1098				assert!(T::Registrar::is_parathread(ParaId::from(i)));
1099			}
1100
1101			Ok(())
1102		}
1103
1104		// Assume that at most 8 people have deposits for leases on a teyrchain.
1105		// This would cover at least 4 years of leases in the worst case scenario.
1106		#[benchmark]
1107		fn clear_all_leases() -> Result<(), BenchmarkError> {
1108			let max_people = 8;
1109			let (para, _) = register_a_parathread::<T>(1);
1110
1111			// If there is an offset, we need to be on that block to be able to do lease things.
1112			pezframe_system::Pezpallet::<T>::set_block_number(T::LeaseOffset::get() + One::one());
1113
1114			for i in 0..max_people {
1115				let leaser = account("lease_deposit", i, 0);
1116				let amount = T::Currency::minimum_balance();
1117				T::Currency::make_free_balance_be(&leaser, BalanceOf::<T>::max_value());
1118
1119				// Average slot has 4 lease periods.
1120				let period_count: LeasePeriodOf<T> = 4u32.into();
1121				let period_begin = period_count * i.into();
1122				let origin = T::ForceOrigin::try_successful_origin()
1123					.expect("ForceOrigin has no successful origin required for the benchmark");
1124				Slots::<T>::force_lease(origin, para, leaser, amount, period_begin, period_count)?;
1125			}
1126
1127			for i in 0..max_people {
1128				let leaser = account("lease_deposit", i, 0);
1129				assert_eq!(T::Currency::reserved_balance(&leaser), T::Currency::minimum_balance());
1130			}
1131
1132			let origin =
1133				T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
1134
1135			#[extrinsic_call]
1136			_(origin as T::RuntimeOrigin, para);
1137
1138			for i in 0..max_people {
1139				let leaser = account("lease_deposit", i, 0);
1140				assert_eq!(T::Currency::reserved_balance(&leaser), 0u32.into());
1141			}
1142
1143			Ok(())
1144		}
1145
1146		#[benchmark]
1147		fn trigger_onboard() -> Result<(), BenchmarkError> {
1148			// get a teyrchain into a bad state where they did not onboard
1149			let (para, _) = register_a_parathread::<T>(1);
1150			Leases::<T>::insert(
1151				para,
1152				vec![Some((
1153					account::<T::AccountId>("lease_insert", 0, 0),
1154					BalanceOf::<T>::default(),
1155				))],
1156			);
1157			assert!(T::Registrar::is_parathread(para));
1158			let caller = whitelisted_caller();
1159
1160			#[extrinsic_call]
1161			_(RawOrigin::Signed(caller), para);
1162
1163			T::Registrar::execute_pending_transitions();
1164			assert!(T::Registrar::is_teyrchain(para));
1165			Ok(())
1166		}
1167
1168		impl_benchmark_test_suite!(
1169			Slots,
1170			crate::integration_tests::new_test_ext(),
1171			crate::integration_tests::Test,
1172		);
1173	}
1174}