In this document we will go step by step on how to integrate on-demand coretime procurement for a parachain.
Basic understanding of the on-demand integration
Polkadot offers two ways for parachains to acquire Coretime: one by purchasing it in bulk from the Coretime chain, and the other through on-demand usage.
Both approaches have their pros and cons and can potentially also be used in combination.
With on-demand, the parachain produces blocks only when an on-demand order is created. Anyone can place an order; however, doing so incurs a cost for the caller.
In the current version, the provided modules offer a solution where collators are expected to place orders. We are dividing time into slots, each assigned to a specific collator responsible for placing an order. Collators are rewarded for placing an order when it is their turn.
Implementation Guide
The following guide explains how to implement on-demand modules for a parachain to automate order creation.
The modules are highly customizable, allowing each parachain to define its own rules for placing orders.
The on-demand pallet is a pallet designed to store configurations related to order creation. It exposes extrinsics that can be called by an AdminOrigin to update the configurations.
Additionally, this pallet is responsible for rewarding collators when they place an order within their slot. This part of the logic is implemented in an inherent, which tries to find the OnDemandOrderPlaced event in the specified relay chain block. If the order exists and the order placer is the expected collator, the pallet will reward the collator.
The following subsections will explain how to add the pallet to your parachain's runtime.
use pallet_on_demand::FixedReward;
use crate::frame_system::EventRecord;
use alloc::{boxed::Box, vec::Vec};
use order_primitives::well_known_keys::EVENTS;
use cumulus_pallet_parachain_system::RelayChainStateProof;
use polkadot_runtime_parachains::on_demand;
/* ... */
#[cfg(feature = "runtime-benchmarks")]
pub struct BenchHelper;
#[cfg(feature = "runtime-benchmarks")]
impl pallet_on_demand::BenchmarkHelper<Balance> for BenchHelper {
fn mock_threshold_parameter() -> Balance {
1_000u32.into()
}
}
pub struct ToAccountIdImpl;
impl Convert<AccountId, AccountId> for ToAccountIdImpl {
fn convert(v: AccountId) -> AccountId {
v
}
}
/// Type implementing the logic for reading order placement events from the relay chain.
pub struct OrderPlacementChecker;
impl pallet_on_demand::OrdersPlaced<Balance, AccountId> for OrderPlacementChecker {
fn orders_placed(
relay_state_proof: RelayChainStateProof,
expected_para_id: ParaId,
) -> Vec<(Balance, AccountId)> {
let events = relay_state_proof
.read_entry::<Vec<Box<EventRecord<rococo_runtime::RuntimeEvent, Hash>>>>(EVENTS, None)
.ok()
.map(|vec| vec.into_iter().filter_map(|event| Some(event)).collect::<Vec<_>>())
.unwrap_or_default();
let result: Vec<(Balance, AccountId)> = events
.into_iter()
.filter_map(|item| match item.event {
rococo_runtime::RuntimeEvent::OnDemandAssignmentProvider(
on_demand::Event::OnDemandOrderPlaced { para_id, spot_price, ordered_by },
) if para_id == expected_para_id => Some((spot_price, ordered_by)),
_ => None,
})
.collect();
result
}
}
impl pallet_on_demand::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
// Define the origin able to update the configuration and parameters:
type AdminOrigin = EnsureRoot<AccountId>;
// Relay chain block number type:
type BlockNumber = BlockNumber;
// The `ThresholdParameter` is a generic type that can be configured here.
//
// The on-demand pallet allows storing a generic parameter on-chain,
// configurable by `AdminOrigin`.
//
// Not all implementations will require this; however, it can be useful
// for many scenarios where the threshold for order creation could for example
// be based on fees collected from ready transactions.
//
// In such case the parameter would define the fee threshold.
//
// If a threshold parameter is not needed it can simply be set to `()`.
type ThresholdParameter = Balance;
// Relay chain balance type.
type RelayChainBalance = Balance;
// The currency that will be used to reward order placers.
type Currency = Balances;
// Logic for rewarding the order placer.
//
// The pallet provides a default implementation that uses `Currency` to reward the order placer.
type OnReward = OnDemand;
// Defines the logic for determining the reward amount.
type RewardSize = FixedReward<Balance, Reward>;
// Type converting `ValidatorId` to `AccountId`.
//
// In most runtimes these two are the same.
type ToAccountId = ToAccountIdImpl;
// Type used to determine whether an order was meant to be placed.
//
// The order placer will only be rewarded if this returns true.
type OrderPlacementCriteria = FeeBasedCriteria<Runtime, ExtrinsicBaseWeight>;
type OrdersPlaced = OrderPlacementChecker;
#[cfg(feature = "runtime-benchmarks")]
// Helper type used in benchmarks to get a mock threshold parameter.
type BenchmarkHelper = BenchHelper;
type WeightInfo = ();
}
Runtime API
Add the following implementation in the runtime/src/apis.rsfile:
In the node/src/service.rs file we will configure OnDemandConfigwhich is passed to the on-demand service.
pub struct OnDemandConfig;
impl order_service::config::OnDemandConfig for OnDemandConfig {
type OrderPlacementCriteria = FeeBasedCriteria;
type AuthorPub = <AuthorityPair as polkadot_sdk::sp_application_crypto::AppCrypto>::Public;
type Block = Block;
type R = Arc<dyn RelayChainInterface>;
type P = ParachainClient;
type ExPool = sc_transaction_pool::TransactionPoolHandle<Block, ParachainClient>;
type Balance = Balance;
type ThresholdParameter = Balance;
}
Below is an example implementation of the order placement criteria. Each parachain is responsible for defining its own rules; however, in this example, we determine whether an order should be placed based on the total fees of ready transactions in the pool. If the threshold is reached, we return true, signaling that an order should be created.
pub struct OrderPlacementCriteria;
impl OrderCriteria for OrderPlacementCriteria {
type Block = Block;
type P = ParachainClient;
type ExPool = sc_transaction_pool::FullPool<Block, ParachainClient>;
// Checks if the fee threshold has been reached.
fn should_place_order(
parachain: &Self::P,
transaction_pool: Arc<Self::ExPool>,
_height: BlockNumber,
) -> bool {
let pending_iterator = transaction_pool.ready();
let block_hash = parachain.usage_info().chain.best_hash;
let mut total_fees = Balance::from(0u32);
for pending_tx in pending_iterator {
let pending_tx_data = pending_tx.data.clone();
let utx_length = pending_tx_data.encode().len() as u32;
let fee_details =
parachain
.runtime_api()
.query_fee_details(block_hash, pending_tx_data, utx_length);
if let Ok(details) = fee_details {
total_fees = total_fees.saturating_add(details.final_fee());
}
}
let Some(fee_threshold) = parachain.runtime_api().threshold_parameter(block_hash).ok() else {
return false;
};
if fee_threshold > 0 {
log::info!(
"{}% of the threshold requirement met",
total_fees.saturating_div(fee_threshold).saturating_mul(100)
);
}
total_fees >= fee_threshold
}
}
Now that we have all the configuration available we can add the code which starts the on-demand service. This should be added within the start_parachain_node function (it might have a different name), at the point where we are conditionally starting consensus participation for the node, call the on-demand service:
/* ... Other Imports ... */
use cumulus_relay_chain_interface::BlockNumber;
use codec::Encode;
use order_primitives::OnDemandRuntimeApi;
use order_service::{
config::{OnDemandSlot, OrderCriteria},
start_on_demand,
};
use polkadot_primitives::Balance;
use polkadot_sdk::{
pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, polkadot_cli::ProvideRuntimeApi,
sc_client_api::UsageProvider, sc_service::TransactionPool,
};
use sp_consensus_aura::sr25519::AuthorityPair;
/* ... */
fn start_consensus(/* ... */) -> Result<(), sc_service::Error> {
// ...
let relay_chain_interface_clone = relay_chain_interface.clone();
let params = AuraParams {
// Add the following to the inherent data providers:
create_inherent_data_providers: move |_, ()| {
let relay_chain_interface = relay_chain_interface_clone.clone();
let order_record_clone = order_record.clone();
async move {
let record = order_record_clone.lock().await;
let order_inherent = order_primitives::OrderInherentData::create_at(
&relay_chain_interface,
record.relay_block_hash,
para_id,
)
.await;
let order_inherent = order_inherent.ok_or_else(|| {
Box::<dyn std::error::Error + Send + Sync>::from(
"Failed to create order inherent",
)
})?;
Ok(order_inherent)
}
},
block_import,
para_client: client.clone(),
// ...
}
// ...
Ok(())
}
/// Start a node with the given parachain `Configuration` and relay chain `Configuration`.
#[sc_tracing::logging::prefix_logs_with("Parachain")]
pub async fn start_parachain_node(
parachain_config: Configuration,
polkadot_config: Configuration,
collator_options: CollatorOptions,
para_id: ParaId,
hwbench: Option<sc_sysinfo::HwBench>,
) -> sc_service::error::Result<(TaskManager, Arc<ParachainClient>)> {
// ...
let relay_rpc =
polkadot_config.rpc.addr.as_ref().and_then(|r| r.first()).map(|f| f.listen_addr);
// ...
if validator {
let order_record =
Arc::new(Mutex::new(OrderRecord { relay_block_hash: None, last_slot: 0 }));
start_on_demand::<OnDemandConfig>(
client.clone(),
para_id,
relay_chain_interface.clone(),
transaction_pool.clone(),
&task_manager,
params.keystore_container.keystore(),
relay_rpc,
order_record.clone(),
)?;
// ^^^^^^ Start the on-demand service
start_consensus(
client.clone(),
backend,
block_import,
prometheus_registry.as_ref(),
telemetry.as_ref().map(|t| t.handle()),
&task_manager,
relay_chain_interface,
transaction_pool,
params.keystore_container.keystore(),
relay_chain_slot_duration,
para_id,
collator_key.expect("Command line arguments do not allow this. qed"),
overseer_handle,
announce_block,
)?;
}
// ...
Ok((task_manager, client))
}
Pallet On-Demand Overview
In this section, we will go over the extrinsics for modifying the on-demand configuration.
1. set_slot_width
An extrinsic for setting the slot width to coordinate order placement among collators. The slot width is defined in relay chain blocks. Each slot has a supposed order placer responsible for placing an order. There is no penalty for not placing an order; however, collators are incentivized to do so through a reward.
The slot width should be set based on the expected time required for a collator to submit an order and for it to be included in a relay chain block.
For example, if set to 4, the expected order placer will change every 4 relay chain blocks.
Extrinsic for setting a threshold parameter of type Config::ThresholdParameter
This parameter can be used in the logic for deciding whether to place an order. We are keeping this generic, as we do not assume any specific logic for determining whether an order should be placed. It is provided simply to facilitate implementations that want to have an on-chain threshold parameter, which can be set by the AdminOrigin.
For example, this could store a 'fee threshold' for a block, where an order should be placed once the threshold is exceeded.
If the AdminOrigin decides that the parachain should rely on bulk Coretime and no longer reward order placement, it can enforce this by calling this extrinsic.
If set to true, the parachain will no longer reward order placements. However, this does not restrict anyone from placing an order; they simply won't receive a reward for doing so.