referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/on_demand/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot 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// Polkadot 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 Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! The parachain on demand assignment module.
18//!
19//! Implements a mechanism for taking in orders for on-demand parachain (previously parathreads)
20//! assignments. This module is not handled by the initializer but is instead instantiated in the
21//! `construct_runtime` macro.
22//!
23//! The module uses a single queue for all on-demand orders. Orders are generally processed in the
24//! order they are received, but with an important constraint: only one order per ParaId can be
25//! assigned in each scheduling round. If multiple orders for the same ParaId exist in the queue,
26//! only the first will be assigned, and subsequent orders for that ParaId will remain queued until
27//! the next round.
28
29use core::mem;
30
31use sp_runtime::traits::Zero;
32mod benchmarking;
33pub mod migration;
34
35extern crate alloc;
36
37use crate::{configuration, paras};
38use alloc::{collections::BTreeSet, vec::Vec};
39use frame_support::{
40	pallet_prelude::*,
41	traits::{
42		defensive_prelude::*,
43		Currency,
44		ExistenceRequirement::{self, AllowDeath, KeepAlive},
45		WithdrawReasons,
46	},
47	PalletId,
48};
49use frame_system::{pallet_prelude::*, Pallet as System};
50use polkadot_primitives::{Id as ParaId, ON_DEMAND_MAX_QUEUE_MAX_SIZE};
51use sp_runtime::{
52	traits::{AccountIdConversion, One, SaturatedConversion},
53	FixedPointNumber, FixedPointOperand, FixedU128, Perbill, Saturating,
54};
55
56pub use pallet::*;
57
58mod mock_helpers;
59#[cfg(test)]
60mod tests;
61
62const LOG_TARGET: &str = "runtime::parachains::on-demand";
63
64pub trait WeightInfo {
65	fn place_order_allow_death() -> Weight;
66	fn place_order_keep_alive() -> Weight;
67	fn place_order_with_credits() -> Weight;
68}
69
70/// A weight info that is only suitable for testing.
71pub struct TestWeightInfo;
72
73impl WeightInfo for TestWeightInfo {
74	fn place_order_allow_death() -> Weight {
75		Weight::MAX
76	}
77
78	fn place_order_keep_alive() -> Weight {
79		Weight::MAX
80	}
81
82	fn place_order_with_credits() -> Weight {
83		Weight::MAX
84	}
85}
86
87/// Defines how the account wants to pay for on-demand.
88#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq)]
89enum PaymentType {
90	/// Use credits to purchase on-demand coretime.
91	Credits,
92	/// Use account's free balance to purchase on-demand coretime.
93	Balance,
94}
95
96/// Shorthand for the Balance type the runtime is using.
97pub type BalanceOf<T> =
98	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
99
100/// All queued on-demand orders.
101#[derive(Encode, Decode, TypeInfo)]
102pub struct OrderQueue<N> {
103	queue: BoundedVec<EnqueuedOrder<N>, ConstU32<ON_DEMAND_MAX_QUEUE_MAX_SIZE>>,
104}
105
106impl<N> OrderQueue<N> {
107	/// Pop `num_cores` from the queue, assuming `now` as the current block number.
108	pub fn pop_assignment_for_cores<T: Config>(
109		&mut self,
110		now: N,
111		mut num_cores: u32,
112	) -> impl Iterator<Item = ParaId>
113	where
114		N: Saturating + Ord + One + Copy,
115	{
116		let mut popped = BTreeSet::new();
117		let mut remaining_orders = Vec::with_capacity(self.queue.len());
118		for order in mem::take(&mut self.queue) {
119			// Order is ready 2 blocks later (asynchronous backing):
120			let ready_at = order.ordered_at.saturating_plus_one().saturating_plus_one();
121			let is_ready = ready_at <= now;
122
123			if num_cores > 0 && is_ready && popped.insert(order.para_id) {
124				num_cores -= 1;
125			} else {
126				remaining_orders.push(order);
127			}
128		}
129		self.queue = BoundedVec::truncate_from(remaining_orders);
130		popped.into_iter()
131	}
132
133	fn new() -> Self {
134		OrderQueue { queue: BoundedVec::new() }
135	}
136
137	/// Try to push an additional order.
138	///
139	/// Fails if queue is already at capacity.
140	fn try_push(&mut self, now: N, para_id: ParaId) -> Result<(), ParaId> {
141		self.queue
142			.try_push(EnqueuedOrder { para_id, ordered_at: now })
143			.map_err(|o| o.para_id)
144	}
145
146	fn len(&self) -> usize {
147		self.queue.len()
148	}
149}
150
151/// Data about a placed on-demand order.
152#[derive(Encode, Decode, TypeInfo)]
153struct EnqueuedOrder<N> {
154	/// The parachain the order was placed for.
155	para_id: ParaId,
156	/// The block number the order came in.
157	ordered_at: N,
158}
159
160/// Queue data for on-demand.
161#[derive(Encode, Decode, TypeInfo)]
162struct OrderStatus<N> {
163	/// Last calculated traffic value.
164	traffic: FixedU128,
165
166	/// Enqueued orders.
167	queue: OrderQueue<N>,
168}
169
170impl<N> Default for OrderStatus<N> {
171	fn default() -> OrderStatus<N> {
172		OrderStatus { traffic: FixedU128::default(), queue: OrderQueue::new() }
173	}
174}
175
176/// Errors that can happen during spot traffic calculation.
177#[derive(PartialEq, Debug)]
178pub enum SpotTrafficCalculationErr {
179	/// The order queue capacity is at 0.
180	QueueCapacityIsZero,
181	/// The queue size is larger than the queue capacity.
182	QueueSizeLargerThanCapacity,
183	/// Arithmetic error during division, either division by 0 or over/underflow.
184	Division,
185}
186
187#[frame_support::pallet]
188pub mod pallet {
189
190	use super::*;
191	use polkadot_primitives::Id as ParaId;
192
193	const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
194
195	#[pallet::pallet]
196	#[pallet::without_storage_info]
197	#[pallet::storage_version(STORAGE_VERSION)]
198	pub struct Pallet<T>(_);
199
200	#[pallet::config]
201	pub trait Config: frame_system::Config + configuration::Config + paras::Config {
202		/// The runtime's definition of an event.
203		#[allow(deprecated)]
204		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
205
206		/// The runtime's definition of a Currency.
207		type Currency: Currency<Self::AccountId>;
208
209		/// Something that provides the weight of this pallet.
210		type WeightInfo: WeightInfo;
211
212		/// The default value for the spot traffic multiplier.
213		#[pallet::constant]
214		type TrafficDefaultValue: Get<FixedU128>;
215
216		/// The maximum number of blocks some historical revenue
217		/// information stored for.
218		#[pallet::constant]
219		type MaxHistoricalRevenue: Get<u32>;
220
221		/// Identifier for the internal revenue balance.
222		#[pallet::constant]
223		type PalletId: Get<PalletId>;
224	}
225
226	/// Priority queue for all orders which don't yet (or not any more) have any core affinity.
227	#[pallet::storage]
228	pub(super) type OrderStatus<T: Config> =
229		StorageValue<_, super::OrderStatus<BlockNumberFor<T>>, ValueQuery>;
230
231	/// Keeps track of accumulated revenue from on demand order sales.
232	#[pallet::storage]
233	pub(super) type Revenue<T: Config> =
234		StorageValue<_, BoundedVec<BalanceOf<T>, T::MaxHistoricalRevenue>, ValueQuery>;
235
236	/// Keeps track of credits owned by each account.
237	#[pallet::storage]
238	pub type Credits<T: Config> =
239		StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;
240
241	#[pallet::event]
242	#[pallet::generate_deposit(pub(super) fn deposit_event)]
243	pub enum Event<T: Config> {
244		/// An order was placed at some spot price amount by orderer ordered_by
245		OnDemandOrderPlaced { para_id: ParaId, spot_price: BalanceOf<T>, ordered_by: T::AccountId },
246		/// The value of the spot price has likely changed
247		SpotPriceSet { spot_price: BalanceOf<T> },
248		/// An account was given credits.
249		AccountCredited { who: T::AccountId, amount: BalanceOf<T> },
250		/// The order queue was unexpectedly full - this indicates a problem with the configuration
251		/// of the on-demand pallet on the Coretime chain. `dropped` indicates how many orders were
252		/// dropped because they couldn't be queued.
253		UnexpectedQueueFull { dropped: u32 },
254		/// A batch of on-demand orders from the broker chain has been queued.
255		BatchQueued { batch: Vec<(ParaId, BlockNumberFor<T>)> },
256	}
257
258	#[pallet::error]
259	pub enum Error<T> {
260		/// The order queue is full, `place_order` will not continue.
261		QueueFull,
262		/// The current spot price is higher than the max amount specified in the `place_order`
263		/// call, making it invalid.
264		SpotPriceHigherThanMaxAmount,
265		/// The account doesn't have enough credits to purchase on-demand coretime.
266		InsufficientCredits,
267	}
268
269	#[pallet::hooks]
270	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
271		fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
272			// Update revenue information storage.
273			Revenue::<T>::mutate(|revenue| {
274				if let Some(overdue) =
275					revenue.force_insert_keep_left(0, 0u32.into()).defensive_unwrap_or(None)
276				{
277					// We have some overdue revenue not claimed by the Coretime Chain, let's
278					// accumulate it at the oldest stored block
279					if let Some(last) = revenue.last_mut() {
280						*last = last.saturating_add(overdue);
281					}
282				}
283			});
284
285			let config = configuration::ActiveConfig::<T>::get();
286			// We need to update the spot traffic on block initialize in order to account for idle
287			// blocks.
288			OrderStatus::<T>::mutate(|order_status| {
289				Self::update_spot_traffic(&config, order_status);
290			});
291
292			// Reads: `Revenue`, `ActiveConfig`, `OrderStatus`
293			// Writes: `Revenue`, `OrderStatus`
294			T::DbWeight::get().reads_writes(3, 2)
295		}
296	}
297
298	#[pallet::call]
299	impl<T: Config> Pallet<T> {
300		/// Create a single on demand core order.
301		/// Will use the spot price for the current block and will reap the account if needed.
302		///
303		/// Parameters:
304		/// - `origin`: The sender of the call, funds will be withdrawn from this account.
305		/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
306		/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
307		///
308		/// Errors:
309		/// - `InsufficientBalance`: from the Currency implementation
310		/// - `QueueFull`
311		/// - `SpotPriceHigherThanMaxAmount`
312		///
313		/// Events:
314		/// - `OnDemandOrderPlaced`
315		#[pallet::call_index(0)]
316		#[pallet::weight(<T as Config>::WeightInfo::place_order_allow_death())]
317		#[allow(deprecated)]
318		#[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")]
319		pub fn place_order_allow_death(
320			origin: OriginFor<T>,
321			max_amount: BalanceOf<T>,
322			para_id: ParaId,
323		) -> DispatchResult {
324			let sender = ensure_signed(origin)?;
325			Pallet::<T>::do_place_order(
326				sender,
327				max_amount,
328				para_id,
329				AllowDeath,
330				PaymentType::Balance,
331			)
332		}
333
334		/// Same as the [`place_order_allow_death`](Self::place_order_allow_death) call , but with a
335		/// check that placing the order will not reap the account.
336		///
337		/// Parameters:
338		/// - `origin`: The sender of the call, funds will be withdrawn from this account.
339		/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
340		/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
341		///
342		/// Errors:
343		/// - `InsufficientBalance`: from the Currency implementation
344		/// - `QueueFull`
345		/// - `SpotPriceHigherThanMaxAmount`
346		///
347		/// Events:
348		/// - `OnDemandOrderPlaced`
349		#[pallet::call_index(1)]
350		#[pallet::weight(<T as Config>::WeightInfo::place_order_keep_alive())]
351		#[allow(deprecated)]
352		#[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")]
353		pub fn place_order_keep_alive(
354			origin: OriginFor<T>,
355			max_amount: BalanceOf<T>,
356			para_id: ParaId,
357		) -> DispatchResult {
358			let sender = ensure_signed(origin)?;
359			Pallet::<T>::do_place_order(
360				sender,
361				max_amount,
362				para_id,
363				KeepAlive,
364				PaymentType::Balance,
365			)
366		}
367
368		/// Create a single on demand core order with credits.
369		/// Will charge the owner's on-demand credit account the spot price for the current block.
370		///
371		/// Parameters:
372		/// - `origin`: The sender of the call, on-demand credits will be withdrawn from this
373		///   account.
374		/// - `max_amount`: The maximum number of credits to spend from the origin to place an
375		///   order.
376		/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
377		///
378		/// Errors:
379		/// - `InsufficientCredits`
380		/// - `QueueFull`
381		/// - `SpotPriceHigherThanMaxAmount`
382		///
383		/// Events:
384		/// - `OnDemandOrderPlaced`
385		#[pallet::call_index(2)]
386		#[pallet::weight(<T as Config>::WeightInfo::place_order_with_credits())]
387		pub fn place_order_with_credits(
388			origin: OriginFor<T>,
389			max_amount: BalanceOf<T>,
390			para_id: ParaId,
391		) -> DispatchResult {
392			let sender = ensure_signed(origin)?;
393			Pallet::<T>::do_place_order(
394				sender,
395				max_amount,
396				para_id,
397				KeepAlive,
398				PaymentType::Credits,
399			)
400		}
401	}
402}
403
404// Internal functions and interface to scheduler/wrapping assignment provider.
405impl<T: Config> Pallet<T>
406where
407	BalanceOf<T>: FixedPointOperand,
408{
409	/// Pop assignments for the given number of on-demand cores in a block.
410	pub fn pop_assignment_for_cores(
411		now: BlockNumberFor<T>,
412		num_cores: u32,
413	) -> impl Iterator<Item = ParaId> {
414		pallet::OrderStatus::<T>::mutate(|order_status| {
415			order_status.queue.pop_assignment_for_cores::<T>(now, num_cores)
416		})
417	}
418
419	/// Look into upcoming orders.
420	///
421	/// The returned `OrderQueue` allows for simulating upcoming
422	/// `pop_assignment_for_cores` calls.
423	///
424	/// **Note**: The current implementation returns the entire queue (up to 10,000 orders).
425	/// Callers typically only need `num_cores * scheduling_lookahead` orders (e.g., 10 cores *
426	/// 5 lookahead = 50 orders). Future implementations should consider adding a limit parameter
427	/// to avoid returning unnecessary data and enable more efficient storage schemes.
428	pub fn peek_order_queue() -> OrderQueue<BlockNumberFor<T>> {
429		pallet::OrderStatus::<T>::get().queue
430	}
431
432	/// Push an order back to the back of the queue.
433	///
434	/// The order could not be served for some reason, give it another chance.
435	///
436	/// Parameters:
437	/// - `para_id`: The para that did not make it.
438	pub fn push_back_order(para_id: ParaId) {
439		pallet::OrderStatus::<T>::mutate(|order_status| {
440			let now = <frame_system::Pallet<T>>::block_number();
441			if let Err(e) = order_status.queue.try_push(now, para_id) {
442				log::debug!(target: LOG_TARGET, "Pushing back order failed (queue too long): {:?}", e);
443			};
444		});
445	}
446
447	/// Adds credits to the specified account.
448	///
449	/// Parameters:
450	/// - `who`: Credit receiver.
451	/// - `amount`: The amount of new credits the account will receive.
452	pub fn credit_account(who: T::AccountId, amount: BalanceOf<T>) {
453		Credits::<T>::mutate(who.clone(), |credits| {
454			*credits = credits.saturating_add(amount);
455		});
456		Pallet::<T>::deposit_event(Event::<T>::AccountCredited { who, amount });
457	}
458
459	/// Helper function for `place_order_*` calls. Used to differentiate between placing orders
460	/// with a keep alive check or to allow the account to be reaped. The amount charged is
461	/// stored to the pallet account to be later paid out as revenue.
462	///
463	/// Parameters:
464	/// - `sender`: The sender of the call, funds will be withdrawn from this account.
465	/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
466	/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
467	/// - `existence_requirement`: Whether or not to ensure that the account will not be reaped.
468	/// - `payment_type`: Defines how the user wants to pay for on-demand.
469	///
470	/// Errors:
471	/// - `InsufficientBalance`: from the Currency implementation
472	/// - `QueueFull`
473	/// - `SpotPriceHigherThanMaxAmount`
474	///
475	/// Events:
476	/// - `OnDemandOrderPlaced`
477	fn do_place_order(
478		sender: <T as frame_system::Config>::AccountId,
479		max_amount: BalanceOf<T>,
480		para_id: ParaId,
481		existence_requirement: ExistenceRequirement,
482		payment_type: PaymentType,
483	) -> DispatchResult {
484		let config = configuration::ActiveConfig::<T>::get();
485
486		pallet::OrderStatus::<T>::mutate(|order_status| {
487			Self::update_spot_traffic(&config, order_status);
488			let traffic = order_status.traffic;
489
490			// Calculate spot price
491			let spot_price: BalanceOf<T> = traffic.saturating_mul_int(
492				config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<T>>(),
493			);
494
495			// Is the current price higher than `max_amount`
496			ensure!(spot_price.le(&max_amount), Error::<T>::SpotPriceHigherThanMaxAmount);
497
498			ensure!(
499				order_status.queue.len() <
500					config.scheduler_params.on_demand_queue_max_size as usize,
501				Error::<T>::QueueFull
502			);
503
504			match payment_type {
505				PaymentType::Balance => {
506					// Charge the sending account the spot price. The amount will be teleported to
507					// the broker chain once it requests revenue information.
508					let amt = T::Currency::withdraw(
509						&sender,
510						spot_price,
511						WithdrawReasons::FEE,
512						existence_requirement,
513					)?;
514
515					// Consume the negative imbalance and deposit it into the pallet account. Make
516					// sure the account preserves even without the existential deposit.
517					let pot = Self::account_id();
518					if !System::<T>::account_exists(&pot) {
519						System::<T>::inc_providers(&pot);
520					}
521					T::Currency::resolve_creating(&pot, amt);
522				},
523				PaymentType::Credits => {
524					let credits = Credits::<T>::get(&sender);
525
526					// Charge the sending account the spot price in credits.
527					let new_credits_value =
528						credits.checked_sub(&spot_price).ok_or(Error::<T>::InsufficientCredits)?;
529
530					if new_credits_value.is_zero() {
531						Credits::<T>::remove(&sender);
532					} else {
533						Credits::<T>::insert(&sender, new_credits_value);
534					}
535				},
536			}
537
538			// Add the amount to the current block's (index 0) revenue information.
539			Revenue::<T>::mutate(|bounded_revenue| {
540				if let Some(current_block) = bounded_revenue.get_mut(0) {
541					*current_block = current_block.saturating_add(spot_price);
542				} else {
543					// Revenue has already been claimed in the same block, including the block
544					// itself. It shouldn't normally happen as revenue claims in the future are
545					// not allowed.
546					bounded_revenue.try_push(spot_price).defensive_ok();
547				}
548			});
549
550			let now = <frame_system::Pallet<T>>::block_number();
551			order_status
552				.queue
553				.try_push(now, para_id)
554				.defensive_map_err(|_| Error::<T>::QueueFull)?;
555
556			Pallet::<T>::deposit_event(Event::<T>::OnDemandOrderPlaced {
557				para_id,
558				spot_price,
559				ordered_by: sender,
560			});
561
562			Ok(())
563		})
564	}
565
566	/// Adds a batch of coretime orders to the queue.
567	pub fn queue_order_batch(batch: &[(ParaId, BlockNumberFor<T>)]) {
568		pallet::OrderStatus::<T>::mutate(|order_status| {
569			// The number of successfully queued orders happens to be the same as the index of the
570			// order being currently processed.
571			for (queued, (para_id, ordered_at)) in batch.iter().enumerate() {
572				if let Err(err) = order_status.queue.try_push(*ordered_at, *para_id) {
573					log::debug!(
574						target: LOG_TARGET,
575						"Error trying to push an order to the queue: {:?}", err
576					);
577					Pallet::<T>::deposit_event(Event::<T>::UnexpectedQueueFull {
578						dropped: (batch.len() - queued) as u32,
579					});
580					return;
581				}
582			}
583			Pallet::<T>::deposit_event(Event::<T>::BatchQueued { batch: batch.to_vec() });
584		});
585	}
586
587	/// Calculate and update spot traffic.
588	fn update_spot_traffic(
589		config: &configuration::HostConfiguration<BlockNumberFor<T>>,
590		order_status: &mut OrderStatus<BlockNumberFor<T>>,
591	) {
592		let old_traffic = order_status.traffic;
593		match Self::calculate_spot_traffic(
594			old_traffic,
595			config.scheduler_params.on_demand_queue_max_size,
596			order_status.queue.len() as u32,
597			config.scheduler_params.on_demand_target_queue_utilization,
598			config.scheduler_params.on_demand_fee_variability,
599		) {
600			Ok(new_traffic) => {
601				// Only update storage on change
602				if new_traffic != old_traffic {
603					order_status.traffic = new_traffic;
604
605					// calculate the new spot price
606					let spot_price: BalanceOf<T> = new_traffic.saturating_mul_int(
607						config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<T>>(),
608					);
609
610					// emit the event for updated new price
611					Pallet::<T>::deposit_event(Event::<T>::SpotPriceSet { spot_price });
612				}
613			},
614			Err(err) => {
615				log::debug!(
616					target: LOG_TARGET,
617					"Error calculating spot traffic: {:?}", err
618				);
619			},
620		};
621	}
622
623	/// The spot price multiplier. This is based on the transaction fee calculations defined in:
624	/// https://research.web3.foundation/Polkadot/overview/token-economics#setting-transaction-fees
625	///
626	/// Parameters:
627	/// - `traffic`: The previously calculated multiplier, can never go below 1.0.
628	/// - `queue_capacity`: The max size of the order book.
629	/// - `queue_size`: How many orders are currently in the order book.
630	/// - `target_queue_utilisation`: How much of the queue_capacity should be ideally occupied,
631	///   expressed in percentages(perbill).
632	/// - `variability`: A variability factor, i.e. how quickly the spot price adjusts. This number
633	///   can be chosen by p/(k*(1-s)) where p is the desired ratio increase in spot price over k
634	///   number of blocks. s is the target_queue_utilisation. A concrete example: v =
635	///   0.05/(20*(1-0.25)) = 0.0033.
636	///
637	/// Returns:
638	/// - A `FixedU128` in the range of  `Config::TrafficDefaultValue` - `FixedU128::MAX` on
639	///   success.
640	///
641	/// Errors:
642	/// - `SpotTrafficCalculationErr::QueueCapacityIsZero`
643	/// - `SpotTrafficCalculationErr::QueueSizeLargerThanCapacity`
644	/// - `SpotTrafficCalculationErr::Division`
645	fn calculate_spot_traffic(
646		traffic: FixedU128,
647		queue_capacity: u32,
648		queue_size: u32,
649		target_queue_utilisation: Perbill,
650		variability: Perbill,
651	) -> Result<FixedU128, SpotTrafficCalculationErr> {
652		// Return early if queue has no capacity.
653		if queue_capacity == 0 {
654			return Err(SpotTrafficCalculationErr::QueueCapacityIsZero);
655		}
656
657		// Return early if queue size is greater than capacity.
658		if queue_size > queue_capacity {
659			return Err(SpotTrafficCalculationErr::QueueSizeLargerThanCapacity);
660		}
661
662		// (queue_size / queue_capacity) - target_queue_utilisation
663		let queue_util_ratio = FixedU128::from_rational(queue_size.into(), queue_capacity.into());
664		let positive = queue_util_ratio >= target_queue_utilisation.into();
665		let queue_util_diff = queue_util_ratio.max(target_queue_utilisation.into()) -
666			queue_util_ratio.min(target_queue_utilisation.into());
667
668		// variability * queue_util_diff
669		let var_times_qud = queue_util_diff.saturating_mul(variability.into());
670
671		// variability^2 * queue_util_diff^2
672		let var_times_qud_pow = var_times_qud.saturating_mul(var_times_qud);
673
674		// (variability^2 * queue_util_diff^2)/2
675		let div_by_two: FixedU128;
676		match var_times_qud_pow.const_checked_div(2.into()) {
677			Some(dbt) => div_by_two = dbt,
678			None => return Err(SpotTrafficCalculationErr::Division),
679		}
680
681		// traffic * (1 + queue_util_diff) + div_by_two
682		if positive {
683			let new_traffic = queue_util_diff
684				.saturating_add(div_by_two)
685				.saturating_add(One::one())
686				.saturating_mul(traffic);
687			Ok(new_traffic.max(<T as Config>::TrafficDefaultValue::get()))
688		} else {
689			let new_traffic = queue_util_diff.saturating_sub(div_by_two).saturating_mul(traffic);
690			Ok(new_traffic.max(<T as Config>::TrafficDefaultValue::get()))
691		}
692	}
693
694	/// Collect the revenue from the `when` blockheight
695	pub fn claim_revenue_until(when: BlockNumberFor<T>) -> BalanceOf<T> {
696		let now = <frame_system::Pallet<T>>::block_number();
697		let mut amount: BalanceOf<T> = BalanceOf::<T>::zero();
698		Revenue::<T>::mutate(|revenue| {
699			while !revenue.is_empty() {
700				let index = (revenue.len() - 1) as u32;
701				if when > now.saturating_sub(index.into()) {
702					amount = amount.saturating_add(revenue.pop().defensive_unwrap_or(0u32.into()));
703				} else {
704					break;
705				}
706			}
707		});
708
709		amount
710	}
711
712	/// Account of the pallet pot, where the funds from instantaneous coretime sale are accumulated.
713	pub fn account_id() -> T::AccountId {
714		T::PalletId::get().into_account_truncating()
715	}
716
717	#[cfg(feature = "runtime-benchmarks")]
718	pub fn populate_queue(para_id: ParaId, num: u32) {
719		let now = <frame_system::Pallet<T>>::block_number();
720		pallet::OrderStatus::<T>::mutate(|order_status| {
721			for _ in 0..num {
722				order_status.queue.try_push(now, para_id).unwrap();
723			}
724		});
725	}
726
727	#[cfg(feature = "runtime-benchmarks")]
728	pub(crate) fn set_revenue(rev: BoundedVec<BalanceOf<T>, T::MaxHistoricalRevenue>) {
729		Revenue::<T>::put(rev);
730	}
731
732	#[cfg(test)]
733	fn set_order_status(new_status: OrderStatus<BlockNumberFor<T>>) {
734		pallet::OrderStatus::<T>::set(new_status);
735	}
736
737	#[cfg(test)]
738	fn get_order_status() -> OrderStatus<BlockNumberFor<T>> {
739		pallet::OrderStatus::<T>::get()
740	}
741
742	#[cfg(test)]
743	fn get_traffic_default_value() -> FixedU128 {
744		<T as Config>::TrafficDefaultValue::get()
745	}
746
747	#[cfg(test)]
748	fn get_revenue() -> Vec<BalanceOf<T>> {
749		Revenue::<T>::get().to_vec()
750	}
751}