379cb741ed
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
814 lines
27 KiB
Rust
814 lines
27 KiB
Rust
// Copyright (C) Parity Technologies (UK) Ltd.
|
|
// This file is part of Pezkuwi.
|
|
|
|
// Pezkuwi is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// Pezkuwi is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! The teyrchain on demand assignment module.
|
|
//!
|
|
//! Implements a mechanism for taking in orders for on-demand teyrchain (previously parathreads)
|
|
//! assignments. This module is not handled by the initializer but is instead instantiated in the
|
|
//! `construct_runtime` macro.
|
|
//!
|
|
//! The module currently limits parallel execution of blocks from the same `ParaId` via
|
|
//! a core affinity mechanism. As long as there exists an affinity for a `CoreIndex` for
|
|
//! a specific `ParaId`, orders for blockspace for that `ParaId` will only be assigned to
|
|
//! that `CoreIndex`.
|
|
//!
|
|
//! NOTE: Once we have elastic scaling implemented we might want to extend this module to support
|
|
//! ignoring core affinity up to a certain extend. This should be opt-in though as the teyrchain
|
|
//! needs to support multiple cores in the same block. If we want to enable a single teyrchain
|
|
//! occupying multiple cores in on-demand, we will likely add a separate order type, where the
|
|
//! intent can be made explicit.
|
|
|
|
use pezsp_runtime::traits::Zero;
|
|
mod benchmarking;
|
|
pub mod migration;
|
|
mod mock_helpers;
|
|
mod types;
|
|
|
|
extern crate alloc;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
use crate::{configuration, paras, scheduler::common::Assignment};
|
|
use alloc::collections::BinaryHeap;
|
|
use core::mem::take;
|
|
use pezframe_support::{
|
|
pezpallet_prelude::*,
|
|
traits::{
|
|
defensive_prelude::*,
|
|
Currency,
|
|
ExistenceRequirement::{self, AllowDeath, KeepAlive},
|
|
WithdrawReasons,
|
|
},
|
|
PalletId,
|
|
};
|
|
use pezframe_system::{pezpallet_prelude::*, Pallet as System};
|
|
use pezkuwi_primitives::{CoreIndex, Id as ParaId};
|
|
use pezsp_runtime::{
|
|
traits::{AccountIdConversion, One, SaturatedConversion},
|
|
FixedPointNumber, FixedPointOperand, FixedU128, Perbill, Saturating,
|
|
};
|
|
use types::{
|
|
BalanceOf, CoreAffinityCount, EnqueuedOrder, QueuePushDirection, QueueStatusType,
|
|
SpotTrafficCalculationErr,
|
|
};
|
|
|
|
const LOG_TARGET: &str = "runtime::teyrchains::on-demand";
|
|
|
|
pub use pallet::*;
|
|
|
|
pub trait WeightInfo {
|
|
fn place_order_allow_death(s: u32) -> Weight;
|
|
fn place_order_keep_alive(s: u32) -> Weight;
|
|
fn place_order_with_credits(s: u32) -> Weight;
|
|
}
|
|
|
|
/// A weight info that is only suitable for testing.
|
|
pub struct TestWeightInfo;
|
|
|
|
impl WeightInfo for TestWeightInfo {
|
|
fn place_order_allow_death(_: u32) -> Weight {
|
|
Weight::MAX
|
|
}
|
|
|
|
fn place_order_keep_alive(_: u32) -> Weight {
|
|
Weight::MAX
|
|
}
|
|
|
|
fn place_order_with_credits(_: u32) -> Weight {
|
|
Weight::MAX
|
|
}
|
|
}
|
|
|
|
/// Defines how the account wants to pay for on-demand.
|
|
#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq)]
|
|
enum PaymentType {
|
|
/// Use credits to purchase on-demand coretime.
|
|
Credits,
|
|
/// Use account's free balance to purchase on-demand coretime.
|
|
Balance,
|
|
}
|
|
|
|
#[pezframe_support::pallet]
|
|
pub mod pallet {
|
|
|
|
use super::*;
|
|
|
|
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
|
|
|
#[pallet::pallet]
|
|
#[pallet::without_storage_info]
|
|
#[pallet::storage_version(STORAGE_VERSION)]
|
|
pub struct Pallet<T>(_);
|
|
|
|
#[pallet::config]
|
|
pub trait Config: pezframe_system::Config + configuration::Config + paras::Config {
|
|
/// The runtime's definition of an event.
|
|
#[allow(deprecated)]
|
|
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
|
|
|
/// The runtime's definition of a Currency.
|
|
type Currency: Currency<Self::AccountId>;
|
|
|
|
/// Something that provides the weight of this pallet.
|
|
type WeightInfo: WeightInfo;
|
|
|
|
/// The default value for the spot traffic multiplier.
|
|
#[pallet::constant]
|
|
type TrafficDefaultValue: Get<FixedU128>;
|
|
|
|
/// The maximum number of blocks some historical revenue
|
|
/// information stored for.
|
|
#[pallet::constant]
|
|
type MaxHistoricalRevenue: Get<u32>;
|
|
|
|
/// Identifier for the internal revenue balance.
|
|
#[pallet::constant]
|
|
type PalletId: Get<PalletId>;
|
|
}
|
|
|
|
/// Creates an empty queue status for an empty queue with initial traffic value.
|
|
#[pallet::type_value]
|
|
pub(super) fn QueueStatusOnEmpty<T: Config>() -> QueueStatusType {
|
|
QueueStatusType { traffic: T::TrafficDefaultValue::get(), ..Default::default() }
|
|
}
|
|
|
|
#[pallet::type_value]
|
|
pub(super) fn EntriesOnEmpty<T: Config>() -> BinaryHeap<EnqueuedOrder> {
|
|
BinaryHeap::new()
|
|
}
|
|
|
|
/// Maps a `ParaId` to `CoreIndex` and keeps track of how many assignments the scheduler has in
|
|
/// it's lookahead. Keeping track of this affinity prevents parallel execution of the same
|
|
/// `ParaId` on two or more `CoreIndex`es.
|
|
#[pallet::storage]
|
|
pub(super) type ParaIdAffinity<T: Config> =
|
|
StorageMap<_, Twox64Concat, ParaId, CoreAffinityCount, OptionQuery>;
|
|
|
|
/// Overall status of queue (both free + affinity entries)
|
|
#[pallet::storage]
|
|
pub(super) type QueueStatus<T: Config> =
|
|
StorageValue<_, QueueStatusType, ValueQuery, QueueStatusOnEmpty<T>>;
|
|
|
|
/// Priority queue for all orders which don't yet (or not any more) have any core affinity.
|
|
#[pallet::storage]
|
|
pub(super) type FreeEntries<T: Config> =
|
|
StorageValue<_, BinaryHeap<EnqueuedOrder>, ValueQuery, EntriesOnEmpty<T>>;
|
|
|
|
/// Queue entries that are currently bound to a particular core due to core affinity.
|
|
#[pallet::storage]
|
|
pub(super) type AffinityEntries<T: Config> = StorageMap<
|
|
_,
|
|
Twox64Concat,
|
|
CoreIndex,
|
|
BinaryHeap<EnqueuedOrder>,
|
|
ValueQuery,
|
|
EntriesOnEmpty<T>,
|
|
>;
|
|
|
|
/// Keeps track of accumulated revenue from on demand order sales.
|
|
#[pallet::storage]
|
|
pub type Revenue<T: Config> =
|
|
StorageValue<_, BoundedVec<BalanceOf<T>, T::MaxHistoricalRevenue>, ValueQuery>;
|
|
|
|
/// Keeps track of credits owned by each account.
|
|
#[pallet::storage]
|
|
pub type Credits<T: Config> =
|
|
StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>;
|
|
|
|
#[pallet::event]
|
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config> {
|
|
/// An order was placed at some spot price amount by orderer ordered_by
|
|
OnDemandOrderPlaced { para_id: ParaId, spot_price: BalanceOf<T>, ordered_by: T::AccountId },
|
|
/// The value of the spot price has likely changed
|
|
SpotPriceSet { spot_price: BalanceOf<T> },
|
|
/// An account was given credits.
|
|
AccountCredited { who: T::AccountId, amount: BalanceOf<T> },
|
|
}
|
|
|
|
#[pallet::error]
|
|
pub enum Error<T> {
|
|
/// The order queue is full, `place_order` will not continue.
|
|
QueueFull,
|
|
/// The current spot price is higher than the max amount specified in the `place_order`
|
|
/// call, making it invalid.
|
|
SpotPriceHigherThanMaxAmount,
|
|
/// The account doesn't have enough credits to purchase on-demand coretime.
|
|
InsufficientCredits,
|
|
}
|
|
|
|
#[pallet::hooks]
|
|
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
|
fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
|
|
// Update revenue information storage.
|
|
Revenue::<T>::mutate(|revenue| {
|
|
if let Some(overdue) =
|
|
revenue.force_insert_keep_left(0, 0u32.into()).defensive_unwrap_or(None)
|
|
{
|
|
// We have some overdue revenue not claimed by the Coretime Chain, let's
|
|
// accumulate it at the oldest stored block
|
|
if let Some(last) = revenue.last_mut() {
|
|
*last = last.saturating_add(overdue);
|
|
}
|
|
}
|
|
});
|
|
|
|
let config = configuration::ActiveConfig::<T>::get();
|
|
// We need to update the spot traffic on block initialize in order to account for idle
|
|
// blocks.
|
|
QueueStatus::<T>::mutate(|queue_status| {
|
|
Self::update_spot_traffic(&config, queue_status);
|
|
});
|
|
|
|
// Reads: `Revenue`, `ActiveConfig`, `QueueStatus`
|
|
// Writes: `Revenue`, `QueueStatus`
|
|
T::DbWeight::get().reads_writes(3, 2)
|
|
}
|
|
}
|
|
|
|
#[pallet::call]
|
|
impl<T: Config> Pallet<T> {
|
|
/// Create a single on demand core order.
|
|
/// Will use the spot price for the current block and will reap the account if needed.
|
|
///
|
|
/// Parameters:
|
|
/// - `origin`: The sender of the call, funds will be withdrawn from this account.
|
|
/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
|
|
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
|
|
///
|
|
/// Errors:
|
|
/// - `InsufficientBalance`: from the Currency implementation
|
|
/// - `QueueFull`
|
|
/// - `SpotPriceHigherThanMaxAmount`
|
|
///
|
|
/// Events:
|
|
/// - `OnDemandOrderPlaced`
|
|
#[pallet::call_index(0)]
|
|
#[pallet::weight(<T as Config>::WeightInfo::place_order_allow_death(QueueStatus::<T>::get().size()))]
|
|
#[allow(deprecated)]
|
|
#[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")]
|
|
pub fn place_order_allow_death(
|
|
origin: OriginFor<T>,
|
|
max_amount: BalanceOf<T>,
|
|
para_id: ParaId,
|
|
) -> DispatchResult {
|
|
let sender = ensure_signed(origin)?;
|
|
Pallet::<T>::do_place_order(
|
|
sender,
|
|
max_amount,
|
|
para_id,
|
|
AllowDeath,
|
|
PaymentType::Balance,
|
|
)
|
|
}
|
|
|
|
/// Same as the [`place_order_allow_death`](Self::place_order_allow_death) call , but with a
|
|
/// check that placing the order will not reap the account.
|
|
///
|
|
/// Parameters:
|
|
/// - `origin`: The sender of the call, funds will be withdrawn from this account.
|
|
/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
|
|
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
|
|
///
|
|
/// Errors:
|
|
/// - `InsufficientBalance`: from the Currency implementation
|
|
/// - `QueueFull`
|
|
/// - `SpotPriceHigherThanMaxAmount`
|
|
///
|
|
/// Events:
|
|
/// - `OnDemandOrderPlaced`
|
|
#[pallet::call_index(1)]
|
|
#[pallet::weight(<T as Config>::WeightInfo::place_order_keep_alive(QueueStatus::<T>::get().size()))]
|
|
#[allow(deprecated)]
|
|
#[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")]
|
|
pub fn place_order_keep_alive(
|
|
origin: OriginFor<T>,
|
|
max_amount: BalanceOf<T>,
|
|
para_id: ParaId,
|
|
) -> DispatchResult {
|
|
let sender = ensure_signed(origin)?;
|
|
Pallet::<T>::do_place_order(
|
|
sender,
|
|
max_amount,
|
|
para_id,
|
|
KeepAlive,
|
|
PaymentType::Balance,
|
|
)
|
|
}
|
|
|
|
/// Create a single on demand core order with credits.
|
|
/// Will charge the owner's on-demand credit account the spot price for the current block.
|
|
///
|
|
/// Parameters:
|
|
/// - `origin`: The sender of the call, on-demand credits will be withdrawn from this
|
|
/// account.
|
|
/// - `max_amount`: The maximum number of credits to spend from the origin to place an
|
|
/// order.
|
|
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
|
|
///
|
|
/// Errors:
|
|
/// - `InsufficientCredits`
|
|
/// - `QueueFull`
|
|
/// - `SpotPriceHigherThanMaxAmount`
|
|
///
|
|
/// Events:
|
|
/// - `OnDemandOrderPlaced`
|
|
#[pallet::call_index(2)]
|
|
#[pallet::weight(<T as Config>::WeightInfo::place_order_with_credits(QueueStatus::<T>::get().size()))]
|
|
pub fn place_order_with_credits(
|
|
origin: OriginFor<T>,
|
|
max_amount: BalanceOf<T>,
|
|
para_id: ParaId,
|
|
) -> DispatchResult {
|
|
let sender = ensure_signed(origin)?;
|
|
Pallet::<T>::do_place_order(
|
|
sender,
|
|
max_amount,
|
|
para_id,
|
|
KeepAlive,
|
|
PaymentType::Credits,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Internal functions and interface to scheduler/wrapping assignment provider.
|
|
impl<T: Config> Pallet<T>
|
|
where
|
|
BalanceOf<T>: FixedPointOperand,
|
|
{
|
|
/// Take the next queued entry that is available for a given core index.
|
|
///
|
|
/// Parameters:
|
|
/// - `core_index`: The core index
|
|
pub fn pop_assignment_for_core(core_index: CoreIndex) -> Option<Assignment> {
|
|
let entry: Result<EnqueuedOrder, ()> = QueueStatus::<T>::try_mutate(|queue_status| {
|
|
AffinityEntries::<T>::try_mutate(core_index, |affinity_entries| {
|
|
let free_entry = FreeEntries::<T>::try_mutate(|free_entries| {
|
|
let affinity_next = affinity_entries.peek();
|
|
let free_next = free_entries.peek();
|
|
let pick_free = match (affinity_next, free_next) {
|
|
(None, _) => true,
|
|
(Some(_), None) => false,
|
|
(Some(a), Some(f)) => f < a,
|
|
};
|
|
if pick_free {
|
|
let entry = free_entries.pop().ok_or(())?;
|
|
let (mut affinities, free): (BinaryHeap<_>, BinaryHeap<_>) =
|
|
take(free_entries)
|
|
.into_iter()
|
|
.partition(|e| e.para_id == entry.para_id);
|
|
affinity_entries.append(&mut affinities);
|
|
*free_entries = free;
|
|
Ok(entry)
|
|
} else {
|
|
Err(())
|
|
}
|
|
});
|
|
let entry = free_entry.or_else(|()| affinity_entries.pop().ok_or(()))?;
|
|
queue_status.consume_index(entry.idx);
|
|
Ok(entry)
|
|
})
|
|
});
|
|
|
|
let assignment = entry.map(|e| Assignment::Pool { para_id: e.para_id, core_index }).ok()?;
|
|
|
|
Pallet::<T>::increase_affinity(assignment.para_id(), core_index);
|
|
Some(assignment)
|
|
}
|
|
|
|
/// Report that an assignment was duplicated by the scheduler.
|
|
pub fn assignment_duplicated(para_id: ParaId, core_index: CoreIndex) {
|
|
Pallet::<T>::increase_affinity(para_id, core_index);
|
|
}
|
|
|
|
/// Report that the `para_id` & `core_index` combination was processed.
|
|
///
|
|
/// This should be called once it is clear that the assignment won't get pushed back anymore.
|
|
///
|
|
/// In other words for each `pop_assignment_for_core` a call to this function or
|
|
/// `push_back_assignment` must follow, but only one.
|
|
pub fn report_processed(para_id: ParaId, core_index: CoreIndex) {
|
|
Pallet::<T>::decrease_affinity_update_queue(para_id, core_index);
|
|
}
|
|
|
|
/// Push an assignment back to the front of the queue.
|
|
///
|
|
/// The assignment has not been processed yet. Typically used on session boundaries.
|
|
///
|
|
/// NOTE: We are not checking queue size here. So due to push backs it is possible that we
|
|
/// exceed the maximum queue size slightly.
|
|
///
|
|
/// Parameters:
|
|
/// - `para_id`: The para that did not make it.
|
|
/// - `core_index`: The core the para was scheduled on.
|
|
pub fn push_back_assignment(para_id: ParaId, core_index: CoreIndex) {
|
|
Pallet::<T>::decrease_affinity_update_queue(para_id, core_index);
|
|
QueueStatus::<T>::mutate(|queue_status| {
|
|
Pallet::<T>::add_on_demand_order(queue_status, para_id, QueuePushDirection::Front);
|
|
});
|
|
}
|
|
|
|
/// Adds credits to the specified account.
|
|
///
|
|
/// Parameters:
|
|
/// - `who`: Credit receiver.
|
|
/// - `amount`: The amount of new credits the account will receive.
|
|
pub fn credit_account(who: T::AccountId, amount: BalanceOf<T>) {
|
|
Credits::<T>::mutate(who.clone(), |credits| {
|
|
*credits = credits.saturating_add(amount);
|
|
});
|
|
Pallet::<T>::deposit_event(Event::<T>::AccountCredited { who, amount });
|
|
}
|
|
|
|
/// Helper function for `place_order_*` calls. Used to differentiate between placing orders
|
|
/// with a keep alive check or to allow the account to be reaped. The amount charged is
|
|
/// stored to the pallet account to be later paid out as revenue.
|
|
///
|
|
/// Parameters:
|
|
/// - `sender`: The sender of the call, funds will be withdrawn from this account.
|
|
/// - `max_amount`: The maximum balance to withdraw from the origin to place an order.
|
|
/// - `para_id`: A `ParaId` the origin wants to provide blockspace for.
|
|
/// - `existence_requirement`: Whether or not to ensure that the account will not be reaped.
|
|
/// - `payment_type`: Defines how the user wants to pay for on-demand.
|
|
///
|
|
/// Errors:
|
|
/// - `InsufficientBalance`: from the Currency implementation
|
|
/// - `QueueFull`
|
|
/// - `SpotPriceHigherThanMaxAmount`
|
|
///
|
|
/// Events:
|
|
/// - `OnDemandOrderPlaced`
|
|
fn do_place_order(
|
|
sender: <T as pezframe_system::Config>::AccountId,
|
|
max_amount: BalanceOf<T>,
|
|
para_id: ParaId,
|
|
existence_requirement: ExistenceRequirement,
|
|
payment_type: PaymentType,
|
|
) -> DispatchResult {
|
|
let config = configuration::ActiveConfig::<T>::get();
|
|
|
|
QueueStatus::<T>::mutate(|queue_status| {
|
|
Self::update_spot_traffic(&config, queue_status);
|
|
let traffic = queue_status.traffic;
|
|
|
|
// Calculate spot price
|
|
let spot_price: BalanceOf<T> = traffic.saturating_mul_int(
|
|
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<T>>(),
|
|
);
|
|
|
|
// Is the current price higher than `max_amount`
|
|
ensure!(spot_price.le(&max_amount), Error::<T>::SpotPriceHigherThanMaxAmount);
|
|
|
|
ensure!(
|
|
queue_status.size() < config.scheduler_params.on_demand_queue_max_size,
|
|
Error::<T>::QueueFull
|
|
);
|
|
|
|
match payment_type {
|
|
PaymentType::Balance => {
|
|
// Charge the sending account the spot price. The amount will be teleported to
|
|
// the broker chain once it requests revenue information.
|
|
let amt = T::Currency::withdraw(
|
|
&sender,
|
|
spot_price,
|
|
WithdrawReasons::FEE,
|
|
existence_requirement,
|
|
)?;
|
|
|
|
// Consume the negative imbalance and deposit it into the pallet account. Make
|
|
// sure the account preserves even without the existential deposit.
|
|
let pot = Self::account_id();
|
|
if !System::<T>::account_exists(&pot) {
|
|
System::<T>::inc_providers(&pot);
|
|
}
|
|
T::Currency::resolve_creating(&pot, amt);
|
|
},
|
|
PaymentType::Credits => {
|
|
let credits = Credits::<T>::get(&sender);
|
|
|
|
// Charge the sending account the spot price in credits.
|
|
let new_credits_value =
|
|
credits.checked_sub(&spot_price).ok_or(Error::<T>::InsufficientCredits)?;
|
|
|
|
if new_credits_value.is_zero() {
|
|
Credits::<T>::remove(&sender);
|
|
} else {
|
|
Credits::<T>::insert(&sender, new_credits_value);
|
|
}
|
|
},
|
|
}
|
|
|
|
// Add the amount to the current block's (index 0) revenue information.
|
|
Revenue::<T>::mutate(|bounded_revenue| {
|
|
if let Some(current_block) = bounded_revenue.get_mut(0) {
|
|
*current_block = current_block.saturating_add(spot_price);
|
|
} else {
|
|
// Revenue has already been claimed in the same block, including the block
|
|
// itself. It shouldn't normally happen as revenue claims in the future are
|
|
// not allowed.
|
|
bounded_revenue.try_push(spot_price).defensive_ok();
|
|
}
|
|
});
|
|
|
|
Pallet::<T>::add_on_demand_order(queue_status, para_id, QueuePushDirection::Back);
|
|
Pallet::<T>::deposit_event(Event::<T>::OnDemandOrderPlaced {
|
|
para_id,
|
|
spot_price,
|
|
ordered_by: sender,
|
|
});
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Calculate and update spot traffic.
|
|
fn update_spot_traffic(
|
|
config: &configuration::HostConfiguration<BlockNumberFor<T>>,
|
|
queue_status: &mut QueueStatusType,
|
|
) {
|
|
let old_traffic = queue_status.traffic;
|
|
match Self::calculate_spot_traffic(
|
|
old_traffic,
|
|
config.scheduler_params.on_demand_queue_max_size,
|
|
queue_status.size(),
|
|
config.scheduler_params.on_demand_target_queue_utilization,
|
|
config.scheduler_params.on_demand_fee_variability,
|
|
) {
|
|
Ok(new_traffic) => {
|
|
// Only update storage on change
|
|
if new_traffic != old_traffic {
|
|
queue_status.traffic = new_traffic;
|
|
|
|
// calculate the new spot price
|
|
let spot_price: BalanceOf<T> = new_traffic.saturating_mul_int(
|
|
config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<T>>(),
|
|
);
|
|
|
|
// emit the event for updated new price
|
|
Pallet::<T>::deposit_event(Event::<T>::SpotPriceSet { spot_price });
|
|
}
|
|
},
|
|
Err(err) => {
|
|
log::debug!(
|
|
target: LOG_TARGET,
|
|
"Error calculating spot traffic: {:?}", err
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
/// The spot price multiplier. This is based on the transaction fee calculations defined in:
|
|
/// https://research.web3.foundation/Polkadot/overview/token-economics#setting-transaction-fees
|
|
///
|
|
/// Parameters:
|
|
/// - `traffic`: The previously calculated multiplier, can never go below 1.0.
|
|
/// - `queue_capacity`: The max size of the order book.
|
|
/// - `queue_size`: How many orders are currently in the order book.
|
|
/// - `target_queue_utilisation`: How much of the queue_capacity should be ideally occupied,
|
|
/// expressed in percentages(perbill).
|
|
/// - `variability`: A variability factor, i.e. how quickly the spot price adjusts. This number
|
|
/// can be chosen by p/(k*(1-s)) where p is the desired ratio increase in spot price over k
|
|
/// number of blocks. s is the target_queue_utilisation. A concrete example: v =
|
|
/// 0.05/(20*(1-0.25)) = 0.0033.
|
|
///
|
|
/// Returns:
|
|
/// - A `FixedU128` in the range of `Config::TrafficDefaultValue` - `FixedU128::MAX` on
|
|
/// success.
|
|
///
|
|
/// Errors:
|
|
/// - `SpotTrafficCalculationErr::QueueCapacityIsZero`
|
|
/// - `SpotTrafficCalculationErr::QueueSizeLargerThanCapacity`
|
|
/// - `SpotTrafficCalculationErr::Division`
|
|
fn calculate_spot_traffic(
|
|
traffic: FixedU128,
|
|
queue_capacity: u32,
|
|
queue_size: u32,
|
|
target_queue_utilisation: Perbill,
|
|
variability: Perbill,
|
|
) -> Result<FixedU128, SpotTrafficCalculationErr> {
|
|
// Return early if queue has no capacity.
|
|
if queue_capacity == 0 {
|
|
return Err(SpotTrafficCalculationErr::QueueCapacityIsZero);
|
|
}
|
|
|
|
// Return early if queue size is greater than capacity.
|
|
if queue_size > queue_capacity {
|
|
return Err(SpotTrafficCalculationErr::QueueSizeLargerThanCapacity);
|
|
}
|
|
|
|
// (queue_size / queue_capacity) - target_queue_utilisation
|
|
let queue_util_ratio = FixedU128::from_rational(queue_size.into(), queue_capacity.into());
|
|
let positive = queue_util_ratio >= target_queue_utilisation.into();
|
|
let queue_util_diff = queue_util_ratio.max(target_queue_utilisation.into()) -
|
|
queue_util_ratio.min(target_queue_utilisation.into());
|
|
|
|
// variability * queue_util_diff
|
|
let var_times_qud = queue_util_diff.saturating_mul(variability.into());
|
|
|
|
// variability^2 * queue_util_diff^2
|
|
let var_times_qud_pow = var_times_qud.saturating_mul(var_times_qud);
|
|
|
|
// (variability^2 * queue_util_diff^2)/2
|
|
let div_by_two: FixedU128;
|
|
match var_times_qud_pow.const_checked_div(2.into()) {
|
|
Some(dbt) => div_by_two = dbt,
|
|
None => return Err(SpotTrafficCalculationErr::Division),
|
|
}
|
|
|
|
// traffic * (1 + queue_util_diff) + div_by_two
|
|
if positive {
|
|
let new_traffic = queue_util_diff
|
|
.saturating_add(div_by_two)
|
|
.saturating_add(One::one())
|
|
.saturating_mul(traffic);
|
|
Ok(new_traffic.max(<T as Config>::TrafficDefaultValue::get()))
|
|
} else {
|
|
let new_traffic = queue_util_diff.saturating_sub(div_by_two).saturating_mul(traffic);
|
|
Ok(new_traffic.max(<T as Config>::TrafficDefaultValue::get()))
|
|
}
|
|
}
|
|
|
|
/// Adds an order to the on demand queue.
|
|
///
|
|
/// Parameters:
|
|
/// - `location`: Whether to push this entry to the back or the front of the queue. Pushing an
|
|
/// entry to the front of the queue is only used when the scheduler wants to push back an
|
|
/// entry it has already popped.
|
|
fn add_on_demand_order(
|
|
queue_status: &mut QueueStatusType,
|
|
para_id: ParaId,
|
|
location: QueuePushDirection,
|
|
) {
|
|
let idx = match location {
|
|
QueuePushDirection::Back => queue_status.push_back(),
|
|
QueuePushDirection::Front => queue_status.push_front(),
|
|
};
|
|
|
|
let affinity = ParaIdAffinity::<T>::get(para_id);
|
|
let order = EnqueuedOrder::new(idx, para_id);
|
|
#[cfg(test)]
|
|
log::debug!(target: LOG_TARGET, "add_on_demand_order, order: {:?}, affinity: {:?}, direction: {:?}", order, affinity, location);
|
|
|
|
match affinity {
|
|
None => FreeEntries::<T>::mutate(|entries| entries.push(order)),
|
|
Some(affinity) =>
|
|
AffinityEntries::<T>::mutate(affinity.core_index, |entries| entries.push(order)),
|
|
}
|
|
}
|
|
|
|
/// Decrease core affinity for para and update queue
|
|
///
|
|
/// if affinity dropped to 0, moving entries back to `FreeEntries`.
|
|
fn decrease_affinity_update_queue(para_id: ParaId, core_index: CoreIndex) {
|
|
let affinity = Pallet::<T>::decrease_affinity(para_id, core_index);
|
|
#[cfg(not(test))]
|
|
debug_assert_ne!(
|
|
affinity, None,
|
|
"Decreased affinity for a para that has not been served on a core?"
|
|
);
|
|
if affinity != Some(0) {
|
|
return;
|
|
}
|
|
// No affinity more for entries on this core, free any entries:
|
|
//
|
|
// This is necessary to ensure them being served as the core might no longer exist at all.
|
|
AffinityEntries::<T>::mutate(core_index, |affinity_entries| {
|
|
FreeEntries::<T>::mutate(|free_entries| {
|
|
let (mut freed, affinities): (BinaryHeap<_>, BinaryHeap<_>) =
|
|
take(affinity_entries).into_iter().partition(|e| e.para_id == para_id);
|
|
free_entries.append(&mut freed);
|
|
*affinity_entries = affinities;
|
|
})
|
|
});
|
|
}
|
|
|
|
/// Decreases the affinity of a `ParaId` to a specified `CoreIndex`.
|
|
///
|
|
/// Subtracts from the count of the `CoreAffinityCount` if an entry is found and the core_index
|
|
/// matches. When the count reaches 0, the entry is removed.
|
|
/// A non-existent entry is a no-op.
|
|
///
|
|
/// Returns: The new affinity of the para on that core. `None` if there is no affinity on this
|
|
/// core.
|
|
fn decrease_affinity(para_id: ParaId, core_index: CoreIndex) -> Option<u32> {
|
|
ParaIdAffinity::<T>::mutate(para_id, |maybe_affinity| {
|
|
let affinity = maybe_affinity.as_mut()?;
|
|
if affinity.core_index == core_index {
|
|
let new_count = affinity.count.saturating_sub(1);
|
|
if new_count > 0 {
|
|
*maybe_affinity = Some(CoreAffinityCount { core_index, count: new_count });
|
|
} else {
|
|
*maybe_affinity = None;
|
|
}
|
|
return Some(new_count);
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Increases the affinity of a `ParaId` to a specified `CoreIndex`.
|
|
/// Adds to the count of the `CoreAffinityCount` if an entry is found and the core_index
|
|
/// matches. A non-existent entry will be initialized with a count of 1 and uses the supplied
|
|
/// `CoreIndex`.
|
|
fn increase_affinity(para_id: ParaId, core_index: CoreIndex) {
|
|
ParaIdAffinity::<T>::mutate(para_id, |maybe_affinity| match maybe_affinity {
|
|
Some(affinity) =>
|
|
if affinity.core_index == core_index {
|
|
*maybe_affinity = Some(CoreAffinityCount {
|
|
core_index,
|
|
count: affinity.count.saturating_add(1),
|
|
});
|
|
},
|
|
None => {
|
|
*maybe_affinity = Some(CoreAffinityCount { core_index, count: 1 });
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Collect the revenue from the `when` blockheight
|
|
pub fn claim_revenue_until(when: BlockNumberFor<T>) -> BalanceOf<T> {
|
|
let now = <pezframe_system::Pallet<T>>::block_number();
|
|
let mut amount: BalanceOf<T> = BalanceOf::<T>::zero();
|
|
Revenue::<T>::mutate(|revenue| {
|
|
while !revenue.is_empty() {
|
|
let index = (revenue.len() - 1) as u32;
|
|
if when > now.saturating_sub(index.into()) {
|
|
amount = amount.saturating_add(revenue.pop().defensive_unwrap_or(0u32.into()));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
amount
|
|
}
|
|
|
|
/// Account of the pallet pot, where the funds from instantaneous coretime sale are accumulated.
|
|
pub fn account_id() -> T::AccountId {
|
|
T::PalletId::get().into_account_truncating()
|
|
}
|
|
|
|
/// Getter for the affinity tracker.
|
|
#[cfg(test)]
|
|
fn get_affinity_map(para_id: ParaId) -> Option<CoreAffinityCount> {
|
|
ParaIdAffinity::<T>::get(para_id)
|
|
}
|
|
|
|
/// Getter for the affinity entries.
|
|
#[cfg(test)]
|
|
fn get_affinity_entries(core_index: CoreIndex) -> BinaryHeap<EnqueuedOrder> {
|
|
AffinityEntries::<T>::get(core_index)
|
|
}
|
|
|
|
/// Getter for the free entries.
|
|
#[cfg(test)]
|
|
fn get_free_entries() -> BinaryHeap<EnqueuedOrder> {
|
|
FreeEntries::<T>::get()
|
|
}
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
pub fn populate_queue(para_id: ParaId, num: u32) {
|
|
QueueStatus::<T>::mutate(|queue_status| {
|
|
for _ in 0..num {
|
|
Pallet::<T>::add_on_demand_order(queue_status, para_id, QueuePushDirection::Back);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn set_queue_status(new_status: QueueStatusType) {
|
|
QueueStatus::<T>::set(new_status);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn get_queue_status() -> QueueStatusType {
|
|
QueueStatus::<T>::get()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn get_traffic_default_value() -> FixedU128 {
|
|
<T as Config>::TrafficDefaultValue::get()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn get_revenue() -> Vec<BalanceOf<T>> {
|
|
Revenue::<T>::get().to_vec()
|
|
}
|
|
}
|