referrerpolicy=no-referrer-when-downgrade

pallet_on_demand_para/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # On-demand Coretime pallet
19//!
20//! Sale of on-demand Coretime from the Coretime chain.
21//!
22//! The Relay chain owns the actual on-demand order queue; this pallet is a front-end for it which
23//! lives alongside [`pallet-broker`](https://docs.rs/pallet-broker) on the Coretime chain:
24//!
25//! - Users call [`Pallet::place_order`] here and pay the spot price in the local currency.
26//! - Because the real queue is one hop away, the spot price cannot be read directly. Instead the
27//!   pallet keeps a local estimate of the queue's depth ([`QueueState`]) which grows with every
28//!   order placed and shrinks by an assumed drain rate per Relay-chain block. The spot price is
29//!   derived from that estimate using [`PriceParameters`].
30//! - Orders accepted within a block are accumulated in [`PendingBatch`] and forwarded to the Relay
31//!   chain in one go on finalization, via [`QueueOnDemandOrders`].
32//!
33//!   NOTE: It is important to make sure that this pallet is ordered before `ParachainSystem` in the
34//!   runtime - otherwise the messages will not be sent in the block in which they are created,
35//!   introducing additional latency.
36
37#![cfg_attr(not(feature = "std"), no_std)]
38
39extern crate alloc;
40
41pub use pallet::*;
42
43mod benchmarking;
44mod types;
45mod weightinfo_extension;
46pub mod weights;
47
48#[cfg(test)]
49mod mock;
50#[cfg(test)]
51mod tests;
52
53use alloc::vec::Vec;
54use fp_coretime::TaskId;
55use frame_support::traits::EnsureOrigin;
56use sp_runtime::traits::BlockNumberProvider;
57
58pub use types::*;
59pub use weightinfo_extension::WeightInfoExt;
60pub use weights::WeightInfo;
61
62/// The default maximum number of outstanding on-demand orders beyond which new orders will be
63/// rejected.
64const DEFAULT_ORDER_CAP: u32 = 100;
65
66/// The default number of orders assumed to be drained out of the order queue per Relay-chain
67/// block.
68const DEFAULT_DRAIN_RATE_PER_BLOCK: u32 = 1;
69
70/// The default percentage by which every additional on-demand order in the queue increases
71/// the spot price for new orders.
72const DEFAULT_PRICE_STEP: u32 = 3;
73
74/// The default base fee for an on-demand order, which will be the spot price when the queue
75/// is empty.
76const DEFAULT_BASE_FEE: u32 = 10_000_000;
77
78/// The Relay-chain block number, as seen by this pallet.
79pub type RelayBlockNumberOf<T> =
80	<<T as Config>::RelayBlockNumberProvider as BlockNumberProvider>::BlockNumber;
81
82/// Instructs the Relay chain to enqueue a batch of on-demand orders.
83///
84/// On the Coretime chain this is implemented by sending an XCM `Transact` to the Relay chain's
85/// coretime pallet.
86pub trait QueueOnDemandOrders<RelayBlockNumber> {
87	/// Enqueue `batch` on the Relay chain.
88	///
89	/// Each entry is the parachain the order was placed for and the Relay-chain block number it was
90	/// ordered at.
91	fn queue_batch(batch: Vec<(TaskId, RelayBlockNumber)>);
92}
93
94impl<RelayBlockNumber> QueueOnDemandOrders<RelayBlockNumber> for () {
95	fn queue_batch(_batch: Vec<(TaskId, RelayBlockNumber)>) {}
96}
97
98#[frame_support::pallet]
99pub mod pallet {
100	use super::*;
101	use frame_support::{
102		pallet_prelude::*,
103		traits::{
104			fungible::{Inspect, Mutate},
105			tokens::{Fortitude::Polite, Preservation::Preserve},
106		},
107		PalletId,
108	};
109	use frame_system::pallet_prelude::*;
110	use sp_arithmetic::traits::{SaturatedConversion, Saturating};
111	use sp_runtime::traits::AccountIdConversion;
112
113	const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
114
115	#[pallet::pallet]
116	#[pallet::storage_version(STORAGE_VERSION)]
117	pub struct Pallet<T>(_);
118
119	#[pallet::config]
120	pub trait Config: frame_system::Config {
121		/// Weight information for all calls of this pallet.
122		type WeightInfo: WeightInfo;
123
124		/// Currency used to pay for on-demand Coretime.
125		type Currency: Mutate<Self::AccountId>;
126
127		/// The origin for administrating this pallet.
128		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
129
130		/// Provider of the current Relay-chain block number.
131		type RelayBlockNumberProvider: BlockNumberProvider;
132
133		/// Provider of the on-demand pool capacity.
134		type PoolCapacityProvider: PoolCapacityProvider;
135
136		/// Provider of the order pricing algorithm.
137		type PricingProvider: PricingProvider<BalanceOf<Self>>;
138
139		/// Used to instruct the Relay chain to enqueue the orders placed here.
140		type OrderQueue: QueueOnDemandOrders<RelayBlockNumberOf<Self>>;
141
142		/// Maximum pending batch size.
143		/// NOTE: Since we don't do chunking, this number of pending orders needs to fit within a
144		/// single XCM message. This way `on_finalize` will never send more than one message.
145		/// Since this pallet is intended to be a temporary solution, and current (as of September
146		/// 2026) usage of the on-demand feature is low to moderate, it is expected not to be
147		/// necessary to extend this functionality to allow more orders in a single block.
148		#[pallet::constant]
149		type MaxBatchSize: Get<u32>;
150
151		/// Identifier from which the internal Pot is generated.
152		#[pallet::constant]
153		type PalletId: Get<PalletId>;
154	}
155
156	/// The configuration used for pricing on-demand Coretime orders.
157	#[pallet::storage]
158	pub type PriceConfig<T> = StorageValue<_, PriceParametersOf<T>, ValueQuery>;
159
160	/// The local estimate of the Relay chain's on-demand order queue.
161	#[pallet::storage]
162	pub type QueueState<T> = StorageValue<_, QueueTrackerOf<T>, OptionQuery>;
163
164	/// Orders placed in the current block, forwarded to the Relay chain on finalization.
165	#[pallet::storage]
166	pub type PendingBatch<T: Config> = StorageValue<
167		_,
168		BoundedVec<EnqueuedOrder<RelayBlockNumberOf<T>>, T::MaxBatchSize>,
169		ValueQuery,
170	>;
171
172	#[pallet::event]
173	#[pallet::generate_deposit(pub(super) fn deposit_event)]
174	pub enum Event<T: Config> {
175		/// An on-demand order was placed at `spot_price` by `ordered_by`.
176		OrderPlaced {
177			/// The parachain the order was placed for.
178			para_id: TaskId,
179			/// The spot price that was paid for the order.
180			spot_price: BalanceOf<T>,
181			/// The account that placed and paid for the order.
182			ordered_by: T::AccountId,
183		},
184	}
185
186	#[pallet::error]
187	pub enum Error<T> {
188		/// The estimated on-demand order queue has reached its order cap.
189		QueueFull,
190		/// The batch of orders pending to be sent to the Relay chain is full.
191		BatchFull,
192		/// The spot price was higher than the maximum amount declared in `place_order`.
193		SpotPriceHigherThanMaxAmount,
194		/// The on-demand pool has no cores assigned.
195		EmptyPool,
196		/// The funds in the account submitting the order were not sufficient to cover the declared
197		/// `max_amount`.
198		/// Note: this can be returned even if the funds are sufficient to cover the actual spot
199		/// price.
200		InsufficientFunds,
201		/// The price parameters allow the price to overflow with too many outstanding orders.
202		OrderPriceCanOverflow,
203	}
204
205	#[pallet::hooks]
206	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
207		fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
208			T::WeightInfo::on_finalize_block_fixed()
209		}
210
211		fn on_finalize(_now: BlockNumberFor<T>) {
212			let batch = PendingBatch::<T>::take().into_inner();
213			if batch.is_empty() {
214				return;
215			}
216
217			T::OrderQueue::queue_batch(
218				batch.into_iter().map(|order| (order.para_id, order.ordered_at)).collect(),
219			);
220		}
221	}
222
223	#[pallet::call(weight(<T as Config>::WeightInfo))]
224	impl<T: Config> Pallet<T> {
225		/// Configure the pallet.
226		///
227		/// - `origin`: Must be Root or pass `AdminOrigin`.
228		/// - `config`: The configuration for this pallet.
229		#[pallet::call_index(0)]
230		pub fn configure(
231			origin: OriginFor<T>,
232			config: PriceParametersOf<T>,
233		) -> DispatchResultWithPostInfo {
234			T::AdminOrigin::ensure_origin_or_root(origin)?;
235			config.validate::<T>()?;
236			PriceConfig::<T>::put(config);
237			Ok(Pays::No.into())
238		}
239
240		/// Place an on-demand Coretime order for `para_id`.
241		///
242		/// The caller is charged the current estimated spot price, which must not exceed
243		/// `max_amount`. The order is forwarded to the Relay chain at the end of the block.
244		///
245		/// - `origin`: Must be a signed account with enough funds to pay the spot price.
246		/// - `para_id`: The parachain to schedule.
247		/// - `max_amount`: The maximum spot price the caller is willing to pay.
248		#[pallet::call_index(1)]
249		#[pallet::weight(
250			<T as Config>::WeightInfo::place_order()
251			.saturating_add(T::WeightInfo::on_finalize_block_per_order())
252		)]
253		pub fn place_order(
254			origin: OriginFor<T>,
255			para_id: TaskId,
256			max_amount: BalanceOf<T>,
257		) -> DispatchResult {
258			let who = ensure_signed(origin)?;
259			// TODO(ahm-v2): add a check that the para_id is valid
260			Self::do_place_order(who, para_id, max_amount)
261		}
262	}
263
264	impl<T: Config> Pallet<T> {
265		/// The account holding the revenue from on-demand Coretime sales.
266		pub fn account_id() -> T::AccountId {
267			T::PalletId::get().into_account_truncating()
268		}
269
270		pub(crate) fn do_place_order(
271			who: T::AccountId,
272			para_id: TaskId,
273			max_amount: BalanceOf<T>,
274		) -> DispatchResult {
275			// Fail early if the batch is already full.
276			ensure!(
277				PendingBatch::<T>::decode_len().unwrap_or(0) < T::MaxBatchSize::get() as usize,
278				Error::<T>::BatchFull
279			);
280			// Fail early if the account can't cover the declared max_amount.
281			ensure!(
282				T::Currency::reducible_balance(&who, Preserve, Polite) >= max_amount,
283				Error::<T>::InsufficientFunds
284			);
285
286			let pool_cores = T::PoolCapacityProvider::pool_cores();
287			// Fail early if the pool is empty.
288			ensure!(pool_cores > 0, Error::<T>::EmptyPool);
289
290			let now = T::RelayBlockNumberProvider::current_block_number();
291			let mut queue_state = QueueState::<T>::get()
292				.unwrap_or(QueueTracker { outstanding_orders: 0, last_updated: now });
293			let pricing_config = PriceConfig::<T>::get();
294
295			// Assume the Relay chain has drained part of the queue since we last looked at it.
296			let elapsed = now.saturating_sub(queue_state.last_updated).saturated_into();
297
298			let drained_orders = pricing_config
299				.drain_rate_per_block
300				.saturating_mul(elapsed)
301				.saturating_mul(pool_cores);
302			let outstanding_orders = queue_state.outstanding_orders.saturating_sub(drained_orders);
303
304			ensure!(outstanding_orders < pricing_config.order_cap, Error::<T>::QueueFull);
305
306			let spot_price = T::PricingProvider::spot_price(&pricing_config, outstanding_orders)?;
307
308			ensure!(spot_price <= max_amount, Error::<T>::SpotPriceHigherThanMaxAmount);
309
310			// Charge the sending account the spot price.
311			T::Currency::transfer(&who, &Self::account_id(), spot_price, Preserve)?;
312
313			// Add the order to the batch that gets sent to the Relay chain on finalization.
314			PendingBatch::<T>::try_mutate(|batch| {
315				batch
316					.try_push(EnqueuedOrder { para_id, ordered_at: now })
317					// should not happen (we check if it's full at the start), but won't hurt to
318					// handle the error
319					.map_err(|_| Error::<T>::BatchFull)
320			})?;
321
322			queue_state.outstanding_orders = outstanding_orders.saturating_add(1);
323			queue_state.last_updated = now;
324			QueueState::<T>::put(queue_state);
325
326			Self::deposit_event(Event::<T>::OrderPlaced { para_id, spot_price, ordered_by: who });
327
328			Ok(())
329		}
330	}
331}