feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit 286de54384
6841 changed files with 1848356 additions and 0 deletions
@@ -0,0 +1,88 @@
// 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/>.
//! Helper functions for tests, also used in runtime-benchmarks.
#![cfg(test)]
use super::*;
use crate::{
mock::MockGenesisConfig,
paras::{ParaGenesisArgs, ParaKind},
};
use sp_runtime::Perbill;
use pezkuwi_primitives::{Balance, HeadData, ValidationCode};
fn default_genesis_config() -> MockGenesisConfig {
MockGenesisConfig {
configuration: crate::configuration::GenesisConfig {
config: crate::configuration::HostConfiguration { ..Default::default() },
},
..Default::default()
}
}
#[derive(Debug)]
pub struct GenesisConfigBuilder {
pub on_demand_cores: u32,
pub on_demand_base_fee: Balance,
pub on_demand_fee_variability: Perbill,
pub on_demand_max_queue_size: u32,
pub on_demand_target_queue_utilization: Perbill,
pub onboarded_on_demand_chains: Vec<pezkuwi_primitives::Id>,
}
impl Default for GenesisConfigBuilder {
fn default() -> Self {
Self {
on_demand_cores: 10,
on_demand_base_fee: 10_000,
on_demand_fee_variability: Perbill::from_percent(1),
on_demand_max_queue_size: 100,
on_demand_target_queue_utilization: Perbill::from_percent(25),
onboarded_on_demand_chains: vec![],
}
}
}
impl GenesisConfigBuilder {
pub(super) fn build(self) -> MockGenesisConfig {
let mut genesis = default_genesis_config();
let config = &mut genesis.configuration.config;
config.scheduler_params.num_cores = self.on_demand_cores;
config.scheduler_params.on_demand_base_fee = self.on_demand_base_fee;
config.scheduler_params.on_demand_fee_variability = self.on_demand_fee_variability;
config.scheduler_params.on_demand_queue_max_size = self.on_demand_max_queue_size;
config.scheduler_params.on_demand_target_queue_utilization =
self.on_demand_target_queue_utilization;
let paras = &mut genesis.paras.paras;
for para_id in self.onboarded_on_demand_chains {
paras.push((
para_id,
ParaGenesisArgs {
genesis_head: HeadData::from(vec![0u8]),
validation_code: ValidationCode::from(vec![0u8]),
para_kind: ParaKind::Parathread,
},
))
}
genesis
}
}
@@ -0,0 +1,479 @@
// 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 coretime assignment module.
//!
//! Handles scheduling of assignments coming from the coretime/broker chain. For on-demand
//! assignments it relies on the separate on-demand assignment provider, where it forwards requests
//! to.
//!
//! `CoreDescriptor` contains pointers to the begin and the end of a list of schedules, together
//! with the currently active assignments.
mod mock_helpers;
#[cfg(test)]
mod tests;
use crate::{
configuration, on_demand,
paras::AssignCoretime,
scheduler::common::{Assignment, AssignmentProvider},
ParaId,
};
use alloc::{vec, vec::Vec};
use frame_support::{defensive, pallet_prelude::*};
use frame_system::pallet_prelude::*;
use pallet_broker::CoreAssignment;
use pezkuwi_primitives::CoreIndex;
use sp_runtime::traits::{One, Saturating};
pub use pallet::*;
/// Fraction expressed as a nominator with an assumed denominator of 57,600.
#[derive(
RuntimeDebug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
)]
pub struct PartsOf57600(u16);
impl PartsOf57600 {
pub const ZERO: Self = Self(0);
pub const FULL: Self = Self(57600);
pub fn new_saturating(v: u16) -> Self {
Self::ZERO.saturating_add(Self(v))
}
pub fn is_full(&self) -> bool {
*self == Self::FULL
}
pub fn saturating_add(self, rhs: Self) -> Self {
let inner = self.0.saturating_add(rhs.0);
if inner > 57600 {
Self(57600)
} else {
Self(inner)
}
}
pub fn saturating_sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
pub fn checked_add(self, rhs: Self) -> Option<Self> {
let inner = self.0.saturating_add(rhs.0);
if inner > 57600 {
None
} else {
Some(Self(inner))
}
}
}
/// Assignments as they are scheduled by block number
///
/// for a particular core.
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
struct Schedule<N> {
// Original assignments
assignments: Vec<(CoreAssignment, PartsOf57600)>,
/// When do our assignments become invalid, if at all?
///
/// If this is `Some`, then this `CoreState` will be dropped at that block number. If this is
/// `None`, then we will keep serving our core assignments in a circle until a new set of
/// assignments is scheduled.
end_hint: Option<N>,
/// The next queued schedule for this core.
///
/// Schedules are forming a queue.
next_schedule: Option<N>,
}
/// Descriptor for a core.
///
/// Contains pointers to first and last schedule into `CoreSchedules` for that core and keeps track
/// of the currently active work as well.
#[derive(Encode, Decode, TypeInfo, Default)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug, Clone))]
struct CoreDescriptor<N> {
/// Meta data about the queued schedules for this core.
queue: Option<QueueDescriptor<N>>,
/// Currently performed work.
current_work: Option<WorkState<N>>,
}
/// Pointers into `CoreSchedules` for a particular core.
///
/// Schedules in `CoreSchedules` form a queue. `Schedule::next_schedule` always pointing to the next
/// item.
#[derive(Encode, Decode, TypeInfo, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug))]
struct QueueDescriptor<N> {
/// First scheduled item, that is not yet active.
first: N,
/// Last scheduled item.
last: N,
}
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug, Clone))]
struct WorkState<N> {
/// Assignments with current state.
///
/// Assignments and book keeping on how much has been served already. We keep track of serviced
/// assignments in order to adhere to the specified ratios.
assignments: Vec<(CoreAssignment, AssignmentState)>,
/// When do our assignments become invalid if at all?
///
/// If this is `Some`, then this `CoreState` will be dropped at that block number. If this is
/// `None`, then we will keep serving our core assignments in a circle until a new set of
/// assignments is scheduled.
end_hint: Option<N>,
/// Position in the assignments we are currently in.
///
/// Aka which core assignment will be popped next on
/// `AssignmentProvider::pop_assignment_for_core`.
pos: u16,
/// Step width
///
/// How much we subtract from `AssignmentState::remaining` for a core served.
step: PartsOf57600,
}
#[derive(Encode, Decode, TypeInfo)]
#[cfg_attr(test, derive(PartialEq, RuntimeDebug, Clone, Copy))]
struct AssignmentState {
/// Ratio of the core this assignment has.
///
/// As initially received via `assign_core`.
ratio: PartsOf57600,
/// How many parts are remaining in this round?
///
/// At the end of each round (in preparation for the next), ratio will be added to remaining.
/// Then every time we get scheduled we subtract a core worth of points. Once we reach 0 or a
/// number lower than what a core is worth (`CoreState::step` size), we move on to the next
/// item in the `Vec`.
///
/// The first round starts with remaining = ratio.
remaining: PartsOf57600,
}
impl<N> From<Schedule<N>> for WorkState<N> {
fn from(schedule: Schedule<N>) -> Self {
let Schedule { assignments, end_hint, next_schedule: _ } = schedule;
let step =
if let Some(min_step_assignment) = assignments.iter().min_by(|a, b| a.1.cmp(&b.1)) {
min_step_assignment.1
} else {
// Assignments empty, should not exist. In any case step size does not matter here:
log::debug!("assignments of a `Schedule` should never be empty.");
PartsOf57600(1)
};
let assignments = assignments
.into_iter()
.map(|(a, ratio)| (a, AssignmentState { ratio, remaining: ratio }))
.collect();
Self { assignments, end_hint, pos: 0, step }
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + configuration::Config + on_demand::Config {}
/// Scheduled assignment sets.
///
/// Assignments as of the given block number. They will go into state once the block number is
/// reached (and replace whatever was in there before).
#[pallet::storage]
pub(super) type CoreSchedules<T: Config> = StorageMap<
_,
Twox256,
(BlockNumberFor<T>, CoreIndex),
Schedule<BlockNumberFor<T>>,
OptionQuery,
>;
/// Assignments which are currently active.
///
/// They will be picked from `PendingAssignments` once we reach the scheduled block number in
/// `PendingAssignments`.
#[pallet::storage]
pub(super) type CoreDescriptors<T: Config> = StorageMap<
_,
Twox256,
CoreIndex,
CoreDescriptor<BlockNumberFor<T>>,
ValueQuery,
GetDefault,
>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::error]
pub enum Error<T> {
AssignmentsEmpty,
/// assign_core is only allowed to append new assignments at the end of already existing
/// ones or update the last entry.
DisallowedInsert,
}
}
impl<T: Config> AssignmentProvider<BlockNumberFor<T>> for Pallet<T> {
fn pop_assignment_for_core(core_idx: CoreIndex) -> Option<Assignment> {
let now = frame_system::Pallet::<T>::block_number();
CoreDescriptors::<T>::mutate(core_idx, |core_state| {
Self::ensure_workload(now, core_idx, core_state);
let work_state = core_state.current_work.as_mut()?;
// Wrap around:
work_state.pos = work_state.pos % work_state.assignments.len() as u16;
let (a_type, a_state) = &mut work_state
.assignments
.get_mut(work_state.pos as usize)
.expect("We limited pos to the size of the vec one line above. qed");
// advance for next pop:
a_state.remaining = a_state.remaining.saturating_sub(work_state.step);
if a_state.remaining < work_state.step {
// Assignment exhausted, need to move to the next and credit remaining for
// next round.
work_state.pos += 1;
// Reset to ratio + still remaining "credits":
a_state.remaining = a_state.remaining.saturating_add(a_state.ratio);
}
match a_type {
CoreAssignment::Idle => None,
CoreAssignment::Pool => on_demand::Pallet::<T>::pop_assignment_for_core(core_idx),
CoreAssignment::Task(para_id) => Some(Assignment::Bulk((*para_id).into())),
}
})
}
fn report_processed(assignment: Assignment) {
match assignment {
Assignment::Pool { para_id, core_index } =>
on_demand::Pallet::<T>::report_processed(para_id, core_index),
Assignment::Bulk(_) => {},
}
}
/// Push an assignment back to the front of the queue.
///
/// The assignment has not been processed yet. Typically used on session boundaries.
/// Parameters:
/// - `assignment`: The on demand assignment.
fn push_back_assignment(assignment: Assignment) {
match assignment {
Assignment::Pool { para_id, core_index } =>
on_demand::Pallet::<T>::push_back_assignment(para_id, core_index),
Assignment::Bulk(_) => {
// Session changes are rough. We just drop assignments that did not make it on a
// session boundary. This seems sensible as bulk is region based. Meaning, even if
// we made the effort catching up on those dropped assignments, this would very
// likely lead to other assignments not getting served at the "end" (when our
// assignment set gets replaced).
},
}
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn get_mock_assignment(_: CoreIndex, para_id: pezkuwi_primitives::Id) -> Assignment {
// Given that we are not tracking anything in `Bulk` assignments, it is safe to always
// return a bulk assignment.
Assignment::Bulk(para_id)
}
fn assignment_duplicated(assignment: &Assignment) {
match assignment {
Assignment::Pool { para_id, core_index } =>
on_demand::Pallet::<T>::assignment_duplicated(*para_id, *core_index),
Assignment::Bulk(_) => {},
}
}
}
impl<T: Config> Pallet<T> {
/// Ensure given workload for core is up to date.
fn ensure_workload(
now: BlockNumberFor<T>,
core_idx: CoreIndex,
descriptor: &mut CoreDescriptor<BlockNumberFor<T>>,
) {
// Workload expired?
if descriptor
.current_work
.as_ref()
.and_then(|w| w.end_hint)
.map_or(false, |e| e <= now)
{
descriptor.current_work = None;
}
let Some(queue) = descriptor.queue else {
// No queue.
return;
};
let mut next_scheduled = queue.first;
if next_scheduled > now {
// Not yet ready.
return;
}
// Update is needed:
let update = loop {
let Some(update) = CoreSchedules::<T>::take((next_scheduled, core_idx)) else {
break None;
};
// Still good?
if update.end_hint.map_or(true, |e| e > now) {
break Some(update);
}
// Move on if possible:
if let Some(n) = update.next_schedule {
next_scheduled = n;
} else {
break None;
}
};
let new_first = update.as_ref().and_then(|u| u.next_schedule);
descriptor.current_work = update.map(Into::into);
descriptor.queue = new_first.map(|new_first| {
QueueDescriptor {
first: new_first,
// `last` stays unaffected, if not empty:
last: queue.last,
}
});
}
/// Append another assignment for a core.
///
/// Important: Only appending is allowed or insertion into the last item. Meaning,
/// all already existing assignments must have a `begin` smaller or equal than the one passed
/// here.
/// Updating the last entry is supported to allow for making a core assignment multiple calls to
/// assign_core. Thus if you have too much interlacing for e.g. a single UMP message you can
/// split that up into multiple messages, each triggering a call to `assign_core`, together
/// forming the total assignment.
///
/// Inserting arbitrarily causes a `DispatchError::DisallowedInsert` error.
// With this restriction this function allows for O(1) complexity. It could easily be lifted, if
// need be and in fact an implementation is available
// [here](https://github.com/paritytech/polkadot-sdk/pull/1694/commits/c0c23b01fd2830910cde92c11960dad12cdff398#diff-0c85a46e448de79a5452395829986ee8747e17a857c27ab624304987d2dde8baR386).
// The problem is that insertion complexity then depends on the size of the existing queue,
// which makes determining weights hard and could lead to issues like overweight blocks (at
// least in theory).
pub fn assign_core(
core_idx: CoreIndex,
begin: BlockNumberFor<T>,
mut assignments: Vec<(CoreAssignment, PartsOf57600)>,
end_hint: Option<BlockNumberFor<T>>,
) -> Result<(), DispatchError> {
// There should be at least one assignment.
ensure!(!assignments.is_empty(), Error::<T>::AssignmentsEmpty);
CoreDescriptors::<T>::mutate(core_idx, |core_descriptor| {
let new_queue = match core_descriptor.queue {
Some(queue) => {
ensure!(begin >= queue.last, Error::<T>::DisallowedInsert);
// Update queue if we are appending:
if begin > queue.last {
CoreSchedules::<T>::mutate((queue.last, core_idx), |schedule| {
if let Some(schedule) = schedule.as_mut() {
debug_assert!(schedule.next_schedule.is_none(), "queue.end was supposed to be the end, so the next item must be `None`!");
schedule.next_schedule = Some(begin);
} else {
defensive!("Queue end entry does not exist?");
}
});
}
CoreSchedules::<T>::mutate((begin, core_idx), |schedule| {
let assignments = if let Some(mut old_schedule) = schedule.take() {
old_schedule.assignments.append(&mut assignments);
old_schedule.assignments
} else {
assignments
};
*schedule = Some(Schedule { assignments, end_hint, next_schedule: None });
});
QueueDescriptor { first: queue.first, last: begin }
},
None => {
// Queue empty, just insert:
CoreSchedules::<T>::insert(
(begin, core_idx),
Schedule { assignments, end_hint, next_schedule: None },
);
QueueDescriptor { first: begin, last: begin }
},
};
core_descriptor.queue = Some(new_queue);
Ok(())
})
}
}
impl<T: Config> AssignCoretime for Pallet<T> {
fn assign_coretime(id: ParaId) -> DispatchResult {
let current_block = frame_system::Pallet::<T>::block_number();
// Add a new core and assign the para to it.
let mut config = configuration::ActiveConfig::<T>::get();
let core = config.scheduler_params.num_cores;
config.scheduler_params.num_cores.saturating_inc();
// `assign_coretime` is only called at genesis or by root, so setting the active
// config here is fine.
configuration::Pallet::<T>::force_set_active_config(config);
let begin = current_block + One::one();
let assignment = vec![(pallet_broker::CoreAssignment::Task(id.into()), PartsOf57600::FULL)];
Pallet::<T>::assign_core(CoreIndex(core), begin, assignment, None)
}
}
@@ -0,0 +1,785 @@
// 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/>.
use super::*;
use crate::{
assigner_coretime::{mock_helpers::GenesisConfigBuilder, pallet::Error, Schedule},
initializer::SessionChangeNotification,
mock::{
new_test_ext, CoretimeAssigner, OnDemand, Paras, ParasShared, RuntimeOrigin, Scheduler,
System, Test,
},
paras::{ParaGenesisArgs, ParaKind},
scheduler::common::Assignment,
};
use frame_support::{assert_noop, assert_ok, pallet_prelude::*};
use pallet_broker::TaskId;
use pezkuwi_primitives::{BlockNumber, Id as ParaId, SessionIndex, ValidationCode};
fn schedule_blank_para(id: ParaId, parakind: ParaKind) {
let validation_code: ValidationCode = vec![1, 2, 3].into();
assert_ok!(Paras::schedule_para_initialize(
id,
ParaGenesisArgs {
genesis_head: Vec::new().into(),
validation_code: validation_code.clone(),
para_kind: parakind,
}
));
assert_ok!(Paras::add_trusted_validation_code(RuntimeOrigin::root(), validation_code));
}
fn run_to_block(
to: BlockNumber,
new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>,
) {
while System::block_number() < to {
let b = System::block_number();
Scheduler::initializer_finalize();
Paras::initializer_finalize(b);
if let Some(notification) = new_session(b + 1) {
let mut notification_with_session_index = notification;
// We will make every session change trigger an action queue. Normally this may require
// 2 or more session changes.
if notification_with_session_index.session_index == SessionIndex::default() {
notification_with_session_index.session_index = ParasShared::scheduled_session();
}
Paras::initializer_on_new_session(&notification_with_session_index);
Scheduler::initializer_on_new_session(&notification_with_session_index);
}
System::on_finalize(b);
System::on_initialize(b + 1);
System::set_block_number(b + 1);
Paras::initializer_initialize(b + 1);
Scheduler::initializer_initialize(b + 1);
// Update the spot traffic and revenue on every block.
OnDemand::on_initialize(b + 1);
// In the real runtime this is expected to be called by the `InclusionInherent` pallet.
Scheduler::advance_claim_queue(&Default::default());
}
}
fn default_test_assignments() -> Vec<(CoreAssignment, PartsOf57600)> {
vec![(CoreAssignment::Idle, PartsOf57600::FULL)]
}
fn default_test_schedule() -> Schedule<BlockNumberFor<Test>> {
Schedule { assignments: default_test_assignments(), end_hint: None, next_schedule: None }
}
#[test]
// Should create new QueueDescriptor and add new schedule to CoreSchedules
fn assign_core_works_with_no_prior_schedule() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Call assign_core
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(),
None,
));
// Check CoreSchedules
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(11u32), core_idx)),
Some(default_test_schedule())
);
// Check QueueDescriptor
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.queue
.as_ref()
.and_then(|q| Some(q.first)),
Some(BlockNumberFor::<Test>::from(11u32))
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx).queue.as_ref().and_then(|q| Some(q.last)),
Some(BlockNumberFor::<Test>::from(11u32))
);
});
}
#[test]
fn end_hint_is_properly_honored() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
vec![(CoreAssignment::Task(1), PartsOf57600::FULL)],
Some(15u32),
));
assert!(
CoretimeAssigner::pop_assignment_for_core(core_idx).is_none(),
"No assignment yet in effect"
);
run_to_block(11, |_| None);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(1.into())),
"Assignment should now be present"
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(1.into())),
"Nothing changed, assignment should still be present"
);
run_to_block(15, |_| None);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
None,
"Assignment should now be gone"
);
// Insert assignment that is already dead:
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
vec![(CoreAssignment::Task(1), PartsOf57600::FULL)],
Some(15u32),
));
// Core should still be empty:
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
None,
"Assignment should now be gone"
);
});
}
#[test]
// Should update last in QueueDescriptor and add new schedule to CoreSchedules
fn assign_core_works_with_prior_schedule() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
let default_with_next_schedule =
Schedule { next_schedule: Some(15u32), ..default_test_schedule() };
// Call assign_core twice
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(15u32),
default_test_assignments(),
None,
));
// Check CoreSchedules for two entries
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(11u32), core_idx)),
Some(default_with_next_schedule)
);
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(15u32), core_idx)),
Some(default_test_schedule())
);
// Check QueueDescriptor
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.queue
.as_ref()
.and_then(|q| Some(q.first)),
Some(BlockNumberFor::<Test>::from(11u32))
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx).queue.as_ref().and_then(|q| Some(q.last)),
Some(BlockNumberFor::<Test>::from(15u32))
);
});
}
#[test]
fn assign_core_enforces_higher_or_equal_block_number() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Call assign core twice to establish some schedules
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(12u32),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(15u32),
vec![(CoreAssignment::Idle, PartsOf57600(28800))],
None,
));
// Call assign core with block number before QueueDescriptor first, expecting an error
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(),
None,
),
Error::<Test>::DisallowedInsert
);
// Call assign core with block number between already scheduled assignments, expecting an
// error
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(13u32),
default_test_assignments(),
None,
),
Error::<Test>::DisallowedInsert
);
// Call assign core again on last entry should work:
assert_eq!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(15u32),
vec![(CoreAssignment::Pool, PartsOf57600(28800))],
None,
),
Ok(())
);
});
}
#[test]
fn assign_core_enforces_well_formed_schedule() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
let empty_assignments: Vec<(CoreAssignment, PartsOf57600)> = vec![];
// Attempting assign_core with malformed assignments such that all error cases
// are tested
assert_noop!(
CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
empty_assignments,
None,
),
Error::<Test>::AssignmentsEmpty
);
});
}
#[test]
fn next_schedule_always_points_to_next_work_plan_item() {
let core_idx = CoreIndex(0);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
let start_1 = 15u32;
let start_2 = 20u32;
let start_3 = 25u32;
let start_4 = 30u32;
let start_5 = 35u32;
let expected_schedule_3 =
Schedule { next_schedule: Some(start_4), ..default_test_schedule() };
let expected_schedule_4 =
Schedule { next_schedule: Some(start_5), ..default_test_schedule() };
let expected_schedule_5 = Schedule {
next_schedule: None,
end_hint: None,
assignments: vec![
(CoreAssignment::Pool, PartsOf57600(28800)),
(CoreAssignment::Idle, PartsOf57600(28800)),
],
};
// Call assign_core for each of five schedules
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_1),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_2),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_3),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_4),
default_test_assignments(),
None,
));
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_5),
vec![(CoreAssignment::Pool, PartsOf57600(28800))],
None,
));
// Test updating last entry once more:
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(start_5),
vec![(CoreAssignment::Idle, PartsOf57600(28800))],
None,
));
// Rotate through the first two schedules
run_to_block(start_1, |n| if n == start_1 { Some(Default::default()) } else { None });
CoretimeAssigner::pop_assignment_for_core(core_idx);
run_to_block(start_2, |n| if n == start_2 { Some(Default::default()) } else { None });
CoretimeAssigner::pop_assignment_for_core(core_idx);
// Use saved starting block numbers to check that schedules chain
// together correctly
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(start_3), core_idx)),
Some(expected_schedule_3)
);
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(start_4), core_idx)),
Some(expected_schedule_4)
);
assert_eq!(
CoreSchedules::<Test>::get((BlockNumberFor::<Test>::from(start_5), core_idx)),
Some(expected_schedule_5)
);
// Check QueueDescriptor
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.queue
.as_ref()
.and_then(|q| Some(q.first)),
Some(start_3)
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx).queue.as_ref().and_then(|q| Some(q.last)),
Some(start_5)
);
});
}
#[test]
fn ensure_workload_works() {
let core_idx = CoreIndex(0);
let test_assignment_state =
AssignmentState { ratio: PartsOf57600::FULL, remaining: PartsOf57600::FULL };
let empty_descriptor: CoreDescriptor<BlockNumberFor<Test>> =
CoreDescriptor { queue: None, current_work: None };
let assignments_queued_descriptor = CoreDescriptor {
queue: Some(QueueDescriptor {
first: BlockNumberFor::<Test>::from(11u32),
last: BlockNumberFor::<Test>::from(11u32),
}),
current_work: None,
};
let assignments_active_descriptor = CoreDescriptor {
queue: None,
current_work: Some(WorkState {
assignments: vec![(CoreAssignment::Pool, test_assignment_state)],
end_hint: Some(BlockNumberFor::<Test>::from(15u32)),
pos: 0,
step: PartsOf57600::FULL,
}),
};
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
let mut core_descriptor: CoreDescriptor<BlockNumberFor<Test>> = empty_descriptor.clone();
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Case 1: No new schedule in CoreSchedules for core
CoretimeAssigner::ensure_workload(10u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, empty_descriptor);
// Case 2: New schedule exists in CoreSchedules for core, but new
// schedule start is not yet reached.
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
vec![(CoreAssignment::Pool, PartsOf57600::FULL)],
Some(BlockNumberFor::<Test>::from(15u32)),
));
// Propagate changes from storage to Core_Descriptor handle. Normally
// pop_assignment_for_core would handle this.
core_descriptor = CoreDescriptors::<Test>::get(core_idx);
CoretimeAssigner::ensure_workload(10u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, assignments_queued_descriptor);
// Case 3: Next schedule exists in CoreSchedules for core. Next starting
// block has been reached. Swaps new WorkState into CoreDescriptors from
// CoreSchedules.
CoretimeAssigner::ensure_workload(11u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, assignments_active_descriptor);
// Case 4: end_hint reached but new schedule start not yet reached. WorkState in
// CoreDescriptor is cleared
CoretimeAssigner::ensure_workload(15u32, core_idx, &mut core_descriptor);
assert_eq!(core_descriptor, empty_descriptor);
});
}
#[test]
fn pop_assignment_for_core_works() {
let para_id = ParaId::from(1);
let core_idx = CoreIndex(0);
let alice = 1u64;
let amt = 10_000_000u128;
let assignments_pool = vec![(CoreAssignment::Pool, PartsOf57600::FULL)];
let assignments_task = vec![(CoreAssignment::Task(para_id.into()), PartsOf57600::FULL)];
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
// Initialize the parathread, wait for it to be ready, then add an
// on demand order to later pop with our Coretime assigner.
schedule_blank_para(para_id, ParaKind::Parathread);
on_demand::Credits::<Test>::insert(&alice, amt);
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
assert_ok!(OnDemand::place_order_with_credits(RuntimeOrigin::signed(alice), amt, para_id));
// Case 1: Assignment idle
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
default_test_assignments(), // Default is Idle
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
assert_eq!(CoretimeAssigner::pop_assignment_for_core(core_idx), None);
// Case 2: Assignment pool
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(21u32),
assignments_pool,
None,
));
run_to_block(21, |n| if n == 21 { Some(Default::default()) } else { None });
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Pool { para_id, core_index: 0.into() })
);
// Case 3: Assignment task
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(31u32),
assignments_task,
None,
));
run_to_block(31, |n| if n == 31 { Some(Default::default()) } else { None });
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(para_id))
);
});
}
#[test]
fn assignment_proportions_in_core_state_work() {
let core_idx = CoreIndex(0);
let task_1 = TaskId::from(1u32);
let task_2 = TaskId::from(2u32);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Task 1 gets 2/3 core usage, while task 2 gets 1/3
let test_assignments = vec![
(CoreAssignment::Task(task_1), PartsOf57600::FULL / 3 * 2),
(CoreAssignment::Task(task_2), PartsOf57600::FULL / 3),
];
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
test_assignments,
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Case 1: Current assignment remaining >= step after pop
{
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.pos)),
Some(0u16)
);
// Consumed step should be 1/3 of core parts, leaving 1/3 remaining
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[0].1.remaining)),
Some(PartsOf57600::FULL / 3)
);
}
// Case 2: Current assignment remaining < step after pop
{
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
// Pos should have incremented, as assignment had remaining < step
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.pos)),
Some(1u16)
);
// Remaining should have started at 1/3 of core work parts. We then subtract
// step (1/3) and add back ratio (2/3), leaving us with 2/3 of core work parts.
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[0].1.remaining)),
Some(PartsOf57600::FULL / 3 * 2)
);
}
// Final check, task 2's turn to be served
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
});
}
#[test]
fn equal_assignments_served_equally() {
let core_idx = CoreIndex(0);
let task_1 = TaskId::from(1u32);
let task_2 = TaskId::from(2u32);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Tasks 1 and 2 get equal work parts
let test_assignments = vec![
(CoreAssignment::Task(task_1), PartsOf57600::FULL / 2),
(CoreAssignment::Task(task_2), PartsOf57600::FULL / 2),
];
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
test_assignments,
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Test that popped assignments alternate between tasks 1 and 2
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
});
}
#[test]
// Checks that core is shared fairly, even in case of `ratio` not being
// divisible by `step` (over multiple rounds).
fn assignment_proportions_indivisible_by_step_work() {
let core_idx = CoreIndex(0);
let task_1 = TaskId::from(1u32);
let ratio_1 = PartsOf57600::FULL / 5 * 3;
let ratio_2 = PartsOf57600::FULL / 5 * 2;
let task_2 = TaskId::from(2u32);
new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| {
run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None });
// Task 1 gets 3/5 core usage, while task 2 gets 2/5. That way
// step is set to 2/5 and task 1 is indivisible by step.
let test_assignments =
vec![(CoreAssignment::Task(task_1), ratio_1), (CoreAssignment::Task(task_2), ratio_2)];
assert_ok!(CoretimeAssigner::assign_core(
core_idx,
BlockNumberFor::<Test>::from(11u32),
test_assignments,
None,
));
run_to_block(11, |n| if n == 11 { Some(Default::default()) } else { None });
// Pop 5 assignments. Should Result in the the following work ordering:
// 1, 2, 1, 1, 2. The remaining parts for each assignment should be same
// at the end as in the beginning.
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_1.into()))
);
assert_eq!(
CoretimeAssigner::pop_assignment_for_core(core_idx),
Some(Assignment::Bulk(task_2.into()))
);
// Remaining should equal ratio for both assignments.
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[0].1.remaining)),
Some(ratio_1)
);
assert_eq!(
CoreDescriptors::<Test>::get(core_idx)
.current_work
.as_ref()
.and_then(|w| Some(w.assignments[1].1.remaining)),
Some(ratio_2)
);
});
}
#[cfg(test)]
impl std::ops::Div<u16> for PartsOf57600 {
type Output = Self;
fn div(self, rhs: u16) -> Self::Output {
if rhs == 0 {
panic!("Cannot divide by zero!");
}
Self(self.0 / rhs)
}
}
#[cfg(test)]
impl std::ops::Mul<u16> for PartsOf57600 {
type Output = Self;
fn mul(self, rhs: u16) -> Self {
Self(self.0 * rhs)
}
}
#[test]
fn parts_of_57600_ops() {
assert!(PartsOf57600::new_saturating(57601).is_full());
assert!(PartsOf57600::FULL.saturating_add(PartsOf57600(1)).is_full());
assert_eq!(PartsOf57600::ZERO.saturating_sub(PartsOf57600(1)), PartsOf57600::ZERO);
assert_eq!(PartsOf57600::FULL.checked_add(PartsOf57600(0)), Some(PartsOf57600::FULL));
assert_eq!(PartsOf57600::FULL.checked_add(PartsOf57600(1)), None);
}